Merge pull request #402 from lintonxue00/main

feat: 添加 R2/S3 渠道容量限制功能
This commit is contained in:
叁月柒
2025-12-30 11:47:00 +08:00
committed by GitHub
98 changed files with 354 additions and 36 deletions
+1
View File
@@ -2,3 +2,4 @@
data/*
node_modules
.DS_Store
docs/Sanyue-ImgHub
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.
+7
View File
@@ -125,6 +125,12 @@ async function deleteFile(env, fileId, cdnUrl, url) {
const db = getDatabase(env);
const img = await db.getWithMetadata(fileId);
// 如果文件记录不存在,直接返回成功(幂等删除)
if (!img) {
console.warn(`File ${fileId} not found in database, skipping delete`);
return true;
}
// 如果是R2渠道的图片,需要删除R2中对应的图片
if (img.metadata?.Channel === 'CloudflareR2') {
const R2DataBase = env.img_r2;
@@ -137,6 +143,7 @@ async function deleteFile(env, fileId, cdnUrl, url) {
}
// 删除数据库中的记录
// 注意:容量统计现在由索引自动维护,删除文件后索引更新时会自动重新计算
await db.delete(fileId);
// 清除CDN缓存
+99
View File
@@ -0,0 +1,99 @@
/**
* 容量配额管理 API
* GET: 获取各渠道容量统计(从索引元数据读取)
* POST: 重新统计容量(触发索引重建)
*/
import { getIndexMeta, rebuildIndex } from '../../utils/indexManager.js';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
export async function onRequest(context) {
const { request, env } = context;
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// GET: 获取容量统计(从索引元数据读取,只需 1 次读取)
if (request.method === 'GET') {
return await getQuotaStats(context);
}
// POST: 重新统计容量(触发索引重建)
if (request.method === 'POST') {
return await recalculateQuota(context);
}
return new Response('Method not allowed', { status: 405, headers: corsHeaders });
}
// 获取各渠道容量统计(从索引元数据读取)
async function getQuotaStats(context) {
try {
const indexMeta = await getIndexMeta(context);
return new Response(JSON.stringify({
success: true,
quotaStats: indexMeta.channelStats || {},
totalSizeMB: indexMeta.totalSizeMB || 0,
totalCount: indexMeta.totalCount || 0,
lastUpdated: indexMeta.lastUpdated
}), {
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
} catch (error) {
return new Response(JSON.stringify({
success: false,
error: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
}
}
// 重新统计容量(触发索引重建,会重新计算所有容量统计)
async function recalculateQuota(context) {
try {
// 重建索引会自动重新计算所有容量统计
const result = await rebuildIndex(context);
if (!result.success) {
return new Response(JSON.stringify({
success: false,
error: result.error || 'Failed to rebuild index'
}), {
status: 500,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
}
// 重建完成后,获取最新的统计数据
const indexMeta = await getIndexMeta(context);
return new Response(JSON.stringify({
success: true,
message: 'Quota recalculated successfully',
channelStats: indexMeta.channelStats || {},
totalSizeMB: indexMeta.totalSizeMB || 0,
totalCount: indexMeta.totalCount || 0,
totalUniqueFiles: result.indexedCount
}), {
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
} catch (error) {
return new Response(JSON.stringify({
success: false,
error: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
});
}
}
+2
View File
@@ -110,6 +110,7 @@ export async function getUploadConfig(db, env) {
if (cfr2Channels[0]) {
cfr2Channels[0].publicUrl = r2.publicUrl
cfr2Channels[0].enabled = r2.enabled
cfr2Channels[0].quota = r2.quota // 保留容量限制配置
}
continue
@@ -153,6 +154,7 @@ export async function getUploadConfig(db, env) {
// 如果环境变量未删除,进行覆盖操作
if (s3Channels[0]) {
s3Channels[0].enabled = s.enabled
s3Channels[0].quota = s.quota // 保留容量限制配置
}
continue
+4 -1
View File
@@ -257,7 +257,10 @@ async function mergeR2ChunksInfo(context, uploadId, completedChunks, metadata) {
// 使用multipart info中的finalFileId更新metadata
const finalFileId = multipartInfo.key;
metadata.Channel = "CloudflareR2";
metadata.ChannelName = "R2_env";
// 从 R2 设置中获取渠道名称
const r2Settings = context.uploadConfig.cfr2;
const r2ChannelName = r2Settings.channels?.[0]?.name || "R2_env";
metadata.ChannelName = r2ChannelName;
metadata.FileSize = (totalSize / 1024 / 1024).toFixed(2);
// 清理multipart info
+9 -5
View File
@@ -20,7 +20,7 @@ export async function onRequest(context) { // Contents of context object
// 读取各项配置,存入 context
const securityConfig = await fetchSecurityConfig(env);
const uploadConfig = await fetchUploadConfig(env);
const uploadConfig = await fetchUploadConfig(env, context);
context.securityConfig = securityConfig;
context.uploadConfig = uploadConfig;
@@ -227,12 +227,16 @@ async function uploadFileToCloudflareR2(context, fullId, metadata, returnLink) {
const R2DataBase = env.img_r2;
// 写入R2数据库
await R2DataBase.put(fullId, formdata.get('file'));
// 写入R2数据库,获取实际存储大小
const r2Object = await R2DataBase.put(fullId, formdata.get('file'));
// 更新metadata
metadata.Channel = "CloudflareR2";
metadata.ChannelName = "R2_env";
metadata.ChannelName = r2Channel.name || "R2_env";
// 使用 R2 返回的实际文件大小
if (r2Object && r2Object.size) {
metadata.FileSize = (r2Object.size / 1024 / 1024).toFixed(2);
}
// 图像审查,采用R2的publicUrl
const R2PublicUrl = r2Channel.publicUrl;
@@ -394,7 +398,7 @@ async function uploadFileToTelegram(context, fullId, metadata, fileExt, fileName
if (fileSize > CHUNK_SIZE) {
// 大文件分片上传
return await uploadLargeFileToTelegram(env, file, fullId, metadata, fileName, fileType, url, returnLink, tgBotToken, tgChatId, tgChannel);
return await uploadLargeFileToTelegram(context, file, fullId, metadata, fileName, fileType, returnLink, tgBotToken, tgChatId, tgChannel);
}
// 由于TG会把gif后缀的文件转为视频,所以需要修改后缀名绕过限制
+2 -1
View File
@@ -178,6 +178,7 @@ export async function purgeCDNCache(env, cdnUrl, url, normalizedFolder) {
}
// 结束上传:清除缓存,维护索引
// 注意:容量统计现在由索引自动维护,不需要单独更新 quota
export async function endUpload(context, fileId, metadata) {
const { env, url } = context;
@@ -186,7 +187,7 @@ export async function endUpload(context, fileId, metadata) {
const normalizedFolder = (url.searchParams.get('uploadFolder') || '').replace(/^\/+/, '').replace(/\/{2,}/g, '/').replace(/\/$/, '');
await purgeCDNCache(env, cdnUrl, url, normalizedFolder);
// 更新文件索引
// 更新文件索引(索引更新时会自动计算容量统计)
await addFileToIndex(context, fileId, metadata);
}
+63 -2
View File
@@ -785,6 +785,46 @@ export async function getIndexInfo(context) {
}
}
/**
* 获取索引元数据(轻量级,只读取 meta,不读取整个索引)
* 用于容量检查等场景,避免读取整个索引
* @param {Object} context - 上下文对象
* @returns {Object} 索引元数据,包含 totalCount, totalSizeMB, channelStats 等
*/
export async function getIndexMeta(context) {
const { env } = context;
const db = getDatabase(env);
try {
const metadataStr = await db.get(INDEX_META_KEY);
if (!metadataStr) {
return {
success: false,
totalCount: 0,
totalSizeMB: 0,
channelStats: {}
};
}
const metadata = JSON.parse(metadataStr);
return {
success: true,
totalCount: metadata.totalCount || 0,
totalSizeMB: metadata.totalSizeMB || 0,
channelStats: metadata.channelStats || {},
lastUpdated: metadata.lastUpdated
};
} catch (error) {
console.error('Error getting index meta:', error);
return {
success: false,
totalCount: 0,
totalSizeMB: 0,
channelStats: {}
};
}
}
/* ============= 原子操作相关函数 ============= */
/**
@@ -1300,10 +1340,31 @@ async function saveChunkedIndex(context, index) {
chunks.push(chunk);
}
// 保存索引元数据
// 计算各渠道容量统计
const channelStats = {};
let totalSizeMB = 0;
for (const file of files) {
const channelName = file.metadata?.ChannelName;
const fileSize = parseFloat(file.metadata?.FileSize) || 0;
totalSizeMB += fileSize;
if (channelName) {
if (!channelStats[channelName]) {
channelStats[channelName] = { usedMB: 0, fileCount: 0 };
}
channelStats[channelName].usedMB += fileSize;
channelStats[channelName].fileCount += 1;
}
}
// 保存索引元数据(包含容量统计)
const metadata = {
lastUpdated: index.lastUpdated,
totalCount: index.totalCount,
totalSizeMB: Math.round(totalSizeMB * 100) / 100,
channelStats,
lastOperationId: index.lastOperationId,
chunkCount: chunks.length,
chunkSize: INDEX_CHUNK_SIZE
@@ -1319,7 +1380,7 @@ async function saveChunkedIndex(context, index) {
await Promise.all(savePromises);
console.log(`Saved chunked index: ${chunks.length} chunks, ${files.length} total files`);
console.log(`Saved chunked index: ${chunks.length} chunks, ${files.length} total files, ${totalSizeMB.toFixed(2)} MB`);
return true;
} catch (error) {
+57 -1
View File
@@ -3,8 +3,57 @@ import { getSecurityConfig } from '../api/manage/sysConfig/security';
import { getPageConfig } from '../api/manage/sysConfig/page';
import { getOthersConfig } from '../api/manage/sysConfig/others';
import { getDatabase } from './databaseAdapter.js';
import { getIndexMeta } from './indexManager.js';
export async function fetchUploadConfig(env) {
/**
* 根据容量限制过滤渠道
* @param {Object} context - 上下文对象(包含 env
* @param {Array} channels - 渠道列表
* @returns {Array} 过滤后的渠道列表
*/
async function filterChannelsByQuota(context, channels) {
// 先检查是否有任何渠道启用了容量限制,如果都没启用则跳过 KV 读取
const hasQuotaEnabled = channels.some(ch => ch.quota?.enabled && ch.quota?.limitGB);
if (!hasQuotaEnabled) {
return channels; // 无需读取 KV,直接返回所有渠道
}
// 获取索引元数据(只需 1 次读取)
const indexMeta = await getIndexMeta(context);
const channelStats = indexMeta.channelStats || {};
const result = [];
for (const channel of channels) {
// 未启用容量限制,直接通过
if (!channel.quota?.enabled || !channel.quota?.limitGB) {
result.push(channel);
continue;
}
try {
// 从索引元数据中获取该渠道的容量统计
const stats = channelStats[channel.name] || { usedMB: 0, fileCount: 0 };
const usedGB = stats.usedMB / 1024;
const limitGB = channel.quota.limitGB;
const threshold = channel.quota.threshold || 95;
// 未超过阈值,渠道可用
if ((usedGB / limitGB) * 100 < threshold) {
result.push(channel);
} else {
console.log(`Channel ${channel.name} quota exceeded: ${usedGB.toFixed(2)}GB / ${limitGB}GB (${threshold}% threshold)`);
}
} catch (error) {
console.error(`Failed to check quota for channel ${channel.name}:`, error);
// 检查失败时保守处理,允许使用该渠道
result.push(channel);
}
}
return result;
}
export async function fetchUploadConfig(env, context = null) {
try {
const db = getDatabase(env);
const settings = await getUploadConfig(db, env);
@@ -13,6 +62,13 @@ export async function fetchUploadConfig(env) {
settings.cfr2.channels = settings.cfr2.channels.filter((channel) => channel.enabled);
settings.s3.channels = settings.s3.channels.filter((channel) => channel.enabled);
// 根据容量限制过滤渠道(仅 R2 和 S3)
// 需要 context 来调用 getIndexMeta
if (context) {
settings.cfr2.channels = await filterChannelsByQuota(context, settings.cfr2.channels);
settings.s3.channels = await filterChannelsByQuota(context, settings.s3.channels);
}
return settings;
} catch (error) {
console.error('Failed to fetch upload config:', error);
+1 -1
View File
@@ -1 +1 @@
<!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/chunk-vendors.780b6559.js"></script><script defer="defer" src="/js/app.da914c09.js"></script><link href="/css/chunk-vendors.4363ed49.css" rel="stylesheet"><link href="/css/app.14879ca1.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>
<!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/chunk-vendors.780b6559.js"></script><script defer="defer" src="/js/app.47599341.js"></script><link href="/css/chunk-vendors.4363ed49.css" rel="stylesheet"><link href="/css/app.14879ca1.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>
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.
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.
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.
File diff suppressed because one or more lines are too long
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[672],{2560:function(e,t,i){i.r(t),i.d(t,{default:function(){return p}});var s=i(2619),n=i(6768);function a(e,t,i,a,o,l){const r=s.A;return(0,n.uX)(),(0,n.Wv)(r,{title:l.loginTitle,fields:o.loginFields,"submit-text":"登录","background-key":"loginBkImg","is-admin":!1,loading:o.isLoading,onSubmit:l.handleLogin},null,8,["title","fields","loading","onSubmit"])}i(4114);var o=i(4570),l=i.n(o),r=i(457),d=i(8401),u={data(){return{isLoading:!1,loginFields:[{key:"password",label:"密码",placeholder:"请输入认证码",type:"password",showPassword:!0,icon:"Lock"}]}},computed:{...(0,d.L8)(["userConfig"]),ownerName(){return this.userConfig?.ownerName||"Sanyue"},loginTitle(){return`登录到 ${this.ownerName} 图床`}},components:{BaseLogin:s.A},methods:{async handleLogin(e){const{password:t}=e,i=""===t?"unset":t;this.isLoading=!0;const s=new Promise(e=>setTimeout(e,500)),n=r.A.post("/api/login",{authCode:t}).then(e=>({res:e})).catch(e=>({err:e}));try{const[e]=await Promise.all([n,s]);e.res&&200===e.res.status?(l().set("authCode",i,"14d"),this.$router.push("/"),this.$message.success("登录成功")):(this.isLoading=!1,this.$message.error("登录失败,请检查密码是否正确"))}catch(a){this.isLoading=!1,this.$message.error("系统错误")}}}},c=i(1241);const h=(0,c.A)(u,[["render",a]]);var p=h},2619:function(e,t,i){i.d(t,{A:function(){return F}});var s=i(6975),n=i(47),a=(i(5331),i(9648),i(9623)),o=(i(6711),i(813)),l=(i(4896),i(2583)),r=i(4453),d=i(6768),u=i(4232),c=i(5130);const h={class:"login-container"},p={class:"login-title",tabindex:"0"},g={class:"input-wrapper"},m={key:0,class:"loading-ring"},f={key:1};function b(e,t,i,b,y,k){const w=r.A,L=l.A,v=o.tk,F=a.WK,C=n.S2,A=s.A;return(0,d.uX)(),(0,d.CE)("div",{class:(0,u.C4)(["login",{"is-focused":y.isFocused}])},[(0,d.bF)(w,{class:"toggle-dark"}),(0,d.bF)(L),(0,d.Lk)("div",h,[(0,d.Lk)("h1",p,(0,u.v_)(i.title),1),((0,d.uX)(!0),(0,d.CE)(d.FK,null,(0,d.pI)(i.fields,(e,t)=>((0,d.uX)(),(0,d.CE)("div",{key:e.key,class:"input-container"},[(0,d.Lk)("label",{class:"input-name",ref_for:!0,ref:`inputLabel${t}`,style:(0,u.Tr)({"--underline-width":y.labelUnderlineWidths[t]+"px"})},(0,u.v_)(e.label),5),(0,d.Lk)("div",g,[(0,d.bF)(F,{modelValue:y.formData[e.key],"onUpdate:modelValue":t=>y.formData[e.key]=t,placeholder:e.placeholder,type:e.type||"text","show-password":e.showPassword,class:"password-input",onKeyup:(0,c.jR)(k.handleSubmit,["enter","native"]),onFocus:k.handleInputFocus,onBlur:k.handleInputBlur},(0,d.eX)({_:2},[e.icon?{name:"prefix",fn:(0,d.k6)(()=>[(0,d.bF)(v,{class:"el-input__icon"},{default:(0,d.k6)(()=>[((0,d.uX)(),(0,d.Wv)((0,d.$y)(e.icon)))]),_:2},1024)]),key:"0"}:void 0]),1032,["modelValue","onUpdate:modelValue","placeholder","type","show-password","onKeyup","onFocus","onBlur"])])]))),128)),(0,d.bF)(C,{class:(0,u.C4)(["submit",{"is-loading":i.loading}]),type:"primary",onClick:k.handleSubmit,disabled:i.loading},{default:(0,d.k6)(()=>[i.loading?((0,d.uX)(),(0,d.CE)("div",m)):((0,d.uX)(),(0,d.CE)("span",f,(0,u.v_)(i.submitText),1))]),_:1},8,["class","onClick","disabled"])]),(0,d.bF)(A,{class:"footer"})],2)}i(8111),i(7588);var y=i(8401),k=i(8903),w={name:"BaseLogin",mixins:[k.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},loading:{type:Boolean,default:!1}},data(){return{formData:{},labelUnderlineWidths:[],isFocused:!1}},computed:{...(0,y.L8)(["userConfig"])},watch:{fields:{handler(){this.$nextTick(()=>{this.calculateLabelWidths()})},deep:!0}},components:{Footer:s.A,ToggleDark:r.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 i=this.$refs[`inputLabel${t}`];if(i&&i[0]){const s=document.createElement("canvas"),n=s.getContext("2d"),a=i[0],o=window.getComputedStyle(a);n.font=`${o.fontWeight} ${o.fontSize} ${o.fontFamily}`;const l=n.measureText(e.label).width;this.labelUnderlineWidths[t]=Math.ceil(l)+3}})})},handleSubmit(){this.loading||this.$emit("submit",{...this.formData})},handleInputFocus(e){this.isFocused=!0;const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.add("focused")}},handleInputBlur(e){this.isFocused=!1;const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.remove("focused")}}}},L=i(1241);const v=(0,L.A)(w,[["render",b],["__scopeId","data-v-ddf8586a"]]);var F=v}}]);
//# sourceMappingURL=672.bf344cbb.js.map
//# sourceMappingURL=672.11bec4e1.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([[698],{2619:function(e,t,s){s.d(t,{A:function(){return F}});var i=s(6975),n=s(47),a=(s(5331),s(9648),s(9623)),o=(s(6711),s(813)),l=(s(4896),s(2583)),r=s(4453),d=s(6768),u=s(4232),c=s(5130);const h={class:"login-container"},p={class:"login-title",tabindex:"0"},m={class:"input-wrapper"},g={key:0,class:"loading-ring"},f={key:1};function b(e,t,s,b,y,k){const w=r.A,L=l.A,v=o.tk,F=a.WK,$=n.S2,A=i.A;return(0,d.uX)(),(0,d.CE)("div",{class:(0,u.C4)(["login",{"is-focused":y.isFocused}])},[(0,d.bF)(w,{class:"toggle-dark"}),(0,d.bF)(L),(0,d.Lk)("div",h,[(0,d.Lk)("h1",p,(0,u.v_)(s.title),1),((0,d.uX)(!0),(0,d.CE)(d.FK,null,(0,d.pI)(s.fields,(e,t)=>((0,d.uX)(),(0,d.CE)("div",{key:e.key,class:"input-container"},[(0,d.Lk)("label",{class:"input-name",ref_for:!0,ref:`inputLabel${t}`,style:(0,u.Tr)({"--underline-width":y.labelUnderlineWidths[t]+"px"})},(0,u.v_)(e.label),5),(0,d.Lk)("div",m,[(0,d.bF)(F,{modelValue:y.formData[e.key],"onUpdate:modelValue":t=>y.formData[e.key]=t,placeholder:e.placeholder,type:e.type||"text","show-password":e.showPassword,class:"password-input",onKeyup:(0,c.jR)(k.handleSubmit,["enter","native"]),onFocus:k.handleInputFocus,onBlur:k.handleInputBlur},(0,d.eX)({_:2},[e.icon?{name:"prefix",fn:(0,d.k6)(()=>[(0,d.bF)(v,{class:"el-input__icon"},{default:(0,d.k6)(()=>[((0,d.uX)(),(0,d.Wv)((0,d.$y)(e.icon)))]),_:2},1024)]),key:"0"}:void 0]),1032,["modelValue","onUpdate:modelValue","placeholder","type","show-password","onKeyup","onFocus","onBlur"])])]))),128)),(0,d.bF)($,{class:(0,u.C4)(["submit",{"is-loading":s.loading}]),type:"primary",onClick:k.handleSubmit,disabled:s.loading},{default:(0,d.k6)(()=>[s.loading?((0,d.uX)(),(0,d.CE)("div",g)):((0,d.uX)(),(0,d.CE)("span",f,(0,u.v_)(s.submitText),1))]),_:1},8,["class","onClick","disabled"])]),(0,d.bF)(A,{class:"footer"})],2)}s(8111),s(7588);var y=s(8401),k=s(8903),w={name:"BaseLogin",mixins:[k.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},loading:{type:Boolean,default:!1}},data(){return{formData:{},labelUnderlineWidths:[],isFocused:!1}},computed:{...(0,y.L8)(["userConfig"])},watch:{fields:{handler(){this.$nextTick(()=>{this.calculateLabelWidths()})},deep:!0}},components:{Footer:i.A,ToggleDark:r.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 i=document.createElement("canvas"),n=i.getContext("2d"),a=s[0],o=window.getComputedStyle(a);n.font=`${o.fontWeight} ${o.fontSize} ${o.fontFamily}`;const l=n.measureText(e.label).width;this.labelUnderlineWidths[t]=Math.ceil(l)+3}})})},handleSubmit(){this.loading||this.$emit("submit",{...this.formData})},handleInputFocus(e){this.isFocused=!0;const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.add("focused")}},handleInputBlur(e){this.isFocused=!1;const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.remove("focused")}}}},L=s(1241);const v=(0,L.A)(w,[["render",b],["__scopeId","data-v-ddf8586a"]]);var F=v},3950:function(e,t,s){s.r(t),s.d(t,{default:function(){return u}});var i=s(2619),n=s(6768);function a(e,t,s,a,o,l){const r=i.A;return(0,n.uX)(),(0,n.Wv)(r,{title:"管理端登录",fields:o.loginFields,"submit-text":"登录","background-key":"adminLoginBkImg","is-admin":!0,loading:o.isLoading,onSubmit:l.handleLogin},null,8,["fields","loading","onSubmit"])}s(4114),s(4979);var o=s(457),l={data(){return{isLoading:!1,loginFields:[{key:"username",label:"用户名",placeholder:"请输入用户名",type:"text",icon:"User"},{key:"password",label:"密码",placeholder:"请输入密码",type:"password",showPassword:!0,icon:"Lock"}]}},components:{BaseLogin:i.A},methods:{async handleLogin(e){const{username:t,password:s}=e,i=btoa(`${t}:${s}`);this.isLoading=!0;const n=new Promise(e=>setTimeout(e,1e3)),a=o.A.get("/api/manage/check",{headers:{Authorization:`Basic ${i}`},withCredentials:!0}).then(e=>({response:e})).catch(e=>({error:e}));try{const[e]=await Promise.all([a,n]);if(e.response&&200===e.response.status)this.$store.commit("setCredentials",i),this.$router.push("/dashboard");else{const t=e.error||new Error("Unknown error");this.isLoading=!1,t.response&&401===t.response.status?this.$message.error("用户名或密码错误"):this.$message.error("服务器错误")}}catch(l){this.isLoading=!1,this.$message.error("系统错误")}}}},r=s(1241);const d=(0,r.A)(l,[["render",a]]);var u=d}}]);
//# sourceMappingURL=698.ca48e466.js.map
//# sourceMappingURL=698.77f859a8.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.
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.
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.
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.