文章详情

返回首页

ws01图床 - 基于Cloudflare的云存储

分享文章 作者: Ws01 创建时间: 2026-05-17 更新时间: 2026-05-17 📝 字数: 161,059 字 👁️ 阅读: 15 次

ws01图床 - 基于Cloudflare的 R2 云存储
步骤一(在 Cloudflare Dashboard 中完成,适合 UI 操作):
1、创建 R2 Bucket:进入 Workers & R2 → R2 → Create bucket,记住 bucket 名称。
2、创建 KV 命名空间:Workers → KV → Create namespace,记下命名空间 ID。
3、创建 Worker:Workers → Create a Worker,新建或编辑现有 Worker。
在 Worker 的 Settings → Variables & secrets 中添加绑定:
(变量名1):R2BUCKET,(变量名2):KVSTORE,(变量名3):ADMIN_PASSWORD:设置为强随机密码
新增 JWT_SECRET 并在代码中替换签名密钥(提高安全性)。

/**
 * ws01图床 - 基于Cloudflare的云存储
 * 基于Cloudflare Worker、R2和KV构建的完整云存储解决方案。
 * 环境变量(在Cloudflare仪表盘中设置):
 * - ADMIN_PASSWORD:管理员登录密码
 * 绑定项(在Cloudflare控制面板中设置):
 * - R2_BUCKET:用于文件存储的R2存储桶绑定
 * -  KV_STORE:用于元数据存储的键值命名空间绑定
 */

// ============================================================================
// 工具函数
// ============================================================================

/**
 * 为ID和令牌生成随机字符串
 */
function generateId(length = 16) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  const randomValues = new Uint8Array(length);
  crypto.getRandomValues(randomValues);
  for (let i = 0; i < length; i++) {
    result += chars[randomValues[i] % chars.length];
  }
  return result;
}

/**
 * 使用SHA-256对密码进行哈希运算
 */
async function hashPassword(password) {
  const encoder = new TextEncoder();
  const data = encoder.encode(password);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

/**
 * 创建JWT令牌
 */
async function createJWT(payload, secret) {
  const header = { alg: 'HS256', typ: 'JWT' };
  const encodedHeader = btoa(JSON.stringify(header)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  const encodedPayload = btoa(JSON.stringify(payload)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  
  const signature = await crypto.subtle.sign(
    'HMAC',
    key,
    encoder.encode(`${encodedHeader}.${encodedPayload}`)
  );
  
  const encodedSignature = btoa(String.fromCharCode(...new Uint8Array(signature)))
    .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  
  return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
}

/**
 * 验证JWT令牌
 */
async function verifyJWT(token, secret) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    
    const [encodedHeader, encodedPayload, encodedSignature] = parts;
    
    const encoder = new TextEncoder();
    const key = await crypto.subtle.importKey(
      'raw',
      encoder.encode(secret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['verify']
    );
    
    const signatureData = Uint8Array.from(atob(encodedSignature.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
    
    const valid = await crypto.subtle.verify(
      'HMAC',
      key,
      signatureData,
      encoder.encode(`${encodedHeader}.${encodedPayload}`)
    );
    
    if (!valid) return null;
    
    const payload = JSON.parse(atob(encodedPayload.replace(/-/g, '+').replace(/_/g, '/')));
    
    // Check expiration
    if (payload.exp && Date.now() > payload.exp) return null;
    
    return payload;
  } catch (e) {
    return null;
  }
}

/**
 * 根据持续时间字符串获取过期时间戳
 */
function getExpirationTime(expiresIn) {
  const now = Date.now();
  switch (expiresIn) {
    case '1h': return now + 60 * 60 * 1000;
    case '1d': return now + 24 * 60 * 60 * 1000;
    case '1m': return now + 30 * 24 * 60 * 60 * 1000;
    case '1y': return now + 365 * 24 * 60 * 60 * 1000;
    case '3y': return now + 3 * 365 * 24 * 60 * 60 * 1000;
    case 'permanent': return null;
    default: return now + 24 * 60 * 60 * 1000;
  }
}

/**
 * 设置文件大小以供显示
 */
function formatFileSize(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

/**
 * 从文件扩展名获取MIME类型
 */
function getMimeType(filename) {
  const ext = filename.split('.').pop().toLowerCase();
  const mimeTypes = {
    'html': 'text/html',
    'css': 'text/css',
    'js': 'application/javascript',
    'json': 'application/json',
    'png': 'image/png',
    'jpg': 'image/jpeg',
    'jpeg': 'image/jpeg',
    'gif': 'image/gif',
    'svg': 'image/svg+xml',
    'webp': 'image/webp',
    'ico': 'image/x-icon',
    'pdf': 'application/pdf',
    'zip': 'application/zip',
    'txt': 'text/plain',
    'md': 'text/markdown',
    'mp3': 'audio/mpeg',
    'mp4': 'video/mp4',
    'webm': 'video/webm',
    'doc': 'application/msword',
    'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'xls': 'application/vnd.ms-excel',
    'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    'ppt': 'application/vnd.ms-powerpoint',
    'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  };
  return mimeTypes[ext] || 'application/octet-stream';
}

/**
 * 安全解码R2密钥的URL路径名段/路径。
 * 浏览器对非ASCII路径字符(例如中文名称)进行百分比编码,
 * 而R2密钥存储为原始UTF-8字符串。
 */
function safeDecodePath(path) {
  try {
    return decodeURIComponent(path);
  } catch (e) {
    return path;
  }
}

/**
 * 根据RFC 5987对内容处置的文件名进行编码。
 */
function encodeRFC5987ValueChars(value) {
  return encodeURIComponent(value).replace(/['()*]/g, char =>
    '%' + char.charCodeAt(0).toString(16).toUpperCase()
  );
}

function createAttachmentDisposition(filename) {
  const fallback = filename
    .replace(/[^\x20-\x7E]/g, '_')
    .replace(/["\\]/g, '_') || 'download';
  return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987ValueChars(filename)}`;
}

function createInlineDisposition(filename) {
  const fallback = filename
    .replace(/[^\x20-\x7E]/g, '_')
    .replace(/["\\]/g, '_') || 'preview';
  return `inline; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987ValueChars(filename)}`;
}

const DIRECTORY_CACHE_PREFIX = 'cache:dir:';

function normalizeDirectoryPath(path) {
  let normalized = path || '/';
  if (!normalized.startsWith('/')) normalized = '/' + normalized;
  normalized = normalized.replace(/\/+/g, '/');
  if (normalized.length > 1 && normalized.endsWith('/')) {
    normalized = normalized.slice(0, -1);
  }
  return normalized || '/';
}

function normalizeItemPath(path) {
  let normalized = path || '/';
  if (!normalized.startsWith('/')) normalized = '/' + normalized;
  normalized = normalized.replace(/\/+/g, '/');
  if (normalized.length > 1 && normalized.endsWith('/')) {
    normalized = normalized.slice(0, -1);
  }
  return normalized;
}

function directoryPathToR2Prefix(path) {
  const normalized = normalizeDirectoryPath(path);
  return normalized === '/' ? '' : normalized.slice(1) + '/';
}

function r2KeyToPath(key) {
  return normalizeItemPath('/' + (key || '').replace(/^\/+/, ''));
}

function parentPathFromItemPath(path) {
  const normalized = normalizeItemPath(path);
  if (normalized === '/') return '/';
  const slashIndex = normalized.lastIndexOf('/');
  return slashIndex <= 0 ? '/' : normalized.slice(0, slashIndex);
}

function nameFromItemPath(path) {
  const normalized = normalizeItemPath(path);
  if (normalized === '/') return '';
  return normalized.slice(normalized.lastIndexOf('/') + 1);
}

function parentPathFromR2Key(key) {
  return parentPathFromItemPath(r2KeyToPath(key));
}

function isoDateString(value) {
  if (!value) return new Date().toISOString();
  if (typeof value.toISOString === 'function') return value.toISOString();
  return new Date(value).toISOString();
}

function cacheItemsToResponse(items, currentPath) {
  const folders = [];
  const files = [];

  for (const item of items) {
    if (item.itemType === 'folder') {
      folders.push({
        name: item.name,
        path: item.path
      });
    } else {
      files.push({
        name: item.name,
        path: item.path,
        size: item.size || 0,
        sizeFormatted: formatFileSize(item.size || 0),
        lastModified: item.lastModified,
        previewType: item.previewType || null
      });
    }
  }

  folders.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'));
  files.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'));

  return { success: true, files, folders, currentPath };
}

async function listDirectoryFromR2(env, dirPath) {
  const currentPath = normalizeDirectoryPath(dirPath);
  const prefix = directoryPathToR2Prefix(currentPath);
  const folderMap = new Map();
  const fileMap = new Map();
  let cursor;

  do {
    const listed = await env.R2_BUCKET.list({ prefix, delimiter: '/', cursor });

    if (listed.delimitedPrefixes) {
      for (const folderPath of listed.delimitedPrefixes) {
        const path = r2KeyToPath(folderPath.slice(0, -1));
        const name = folderPath.slice(prefix.length, -1);
        if (name) {
          folderMap.set(path, {
            itemType: 'folder',
            name,
            path,
            parentPath: currentPath,
            r2Key: folderPath.slice(0, -1),
            size: 0,
            contentType: null,
            previewType: null,
            lastModified: null
          });
        }
      }
    }

    if (listed.objects) {
      for (const obj of listed.objects) {
        const name = obj.key.slice(prefix.length);
        if (!name || name === '.folder' || name.includes('/')) continue;

        const path = r2KeyToPath(obj.key);
        fileMap.set(path, {
          itemType: 'file',
          name,
          path,
          parentPath: currentPath,
          r2Key: obj.key,
          size: obj.size || 0,
          contentType: obj.httpMetadata?.contentType || getMimeType(name),
          previewType: getPreviewType(name),
          lastModified: isoDateString(obj.uploaded)
        });
      }
    }

    cursor = listed.truncated ? listed.cursor : null;
  } while (cursor);

  const items = [...folderMap.values(), ...fileMap.values()];
  return {
    ...cacheItemsToResponse(items, currentPath),
    items
  };
}

function directoryCacheKey(dirPath) {
  return DIRECTORY_CACHE_PREFIX + normalizeDirectoryPath(dirPath);
}

async function readDirectoryCache(env, dirPath) {
  const currentPath = normalizeDirectoryPath(dirPath);
  const cachedValue = await env.KV_STORE.get(directoryCacheKey(currentPath), 'json');
  let cached = cachedValue;

  if (!cached) return null;
  if (typeof cached === 'string') {
    cached = JSON.parse(cached);
  }
  if (cached.currentPath !== currentPath) return null;

  const files = Array.isArray(cached.files) ? cached.files : [];
  const folders = Array.isArray(cached.folders) ? cached.folders : [];
  return {
    success: true,
    files,
    folders,
    currentPath,
    cached: true,
    refreshedAt: cached.refreshedAt || null
  };
}

async function writeDirectoryCache(env, dirPath, listing) {
  const currentPath = normalizeDirectoryPath(dirPath);
  const payload = {
    success: true,
    files: listing.files || [],
    folders: listing.folders || [],
    currentPath,
    refreshedAt: Date.now()
  };

  await env.KV_STORE.put(directoryCacheKey(currentPath), JSON.stringify(payload));
  return {
    ...payload,
    cached: false
  };
}

async function deleteDirectoryCache(env, dirPath) {
  await env.KV_STORE.delete(directoryCacheKey(dirPath));
}

async function deleteDirectoryCacheTree(env, dirPath) {
  const currentPath = normalizeDirectoryPath(dirPath);
  const exactKey = directoryCacheKey(currentPath);
  const prefix = currentPath === '/'
    ? DIRECTORY_CACHE_PREFIX
    : exactKey + '/';

  await env.KV_STORE.delete(exactKey);

  let cursor;
  do {
    const listed = await env.KV_STORE.list({ prefix, cursor });
    const keys = listed.keys || [];
    for (const key of keys) {
      await env.KV_STORE.delete(key.name);
    }
    cursor = listed.list_complete ? null : listed.cursor;
  } while (cursor);
}

async function invalidateCachePath(env, path) {
  try {
    const itemPath = normalizeItemPath(path);
    const parentPath = parentPathFromItemPath(itemPath);
    await deleteDirectoryCache(env, parentPath);
    await deleteDirectoryCacheTree(env, itemPath);
  } catch (e) {
    console.warn('KV directory cache invalidation failed:', e.message);
  }
}

async function refreshDirectoryCache(env, dirPath) {
  const currentPath = normalizeDirectoryPath(dirPath);
  const listing = await listDirectoryFromR2(env, currentPath);

  try {
    const previous = await readDirectoryCache(env, currentPath).catch(() => null);
    const newFolderPaths = new Set((listing.folders || []).map(folder => folder.path));

    if (previous) {
      for (const folder of previous.folders || []) {
        if (!newFolderPaths.has(folder.path)) {
          await deleteDirectoryCacheTree(env, folder.path);
        }
      }
    }

    return await writeDirectoryCache(env, currentPath, listing);
  } catch (e) {
    console.warn('KV directory cache refresh failed:', e.message);
    return {
      success: true,
      files: listing.files,
      folders: listing.folders,
      currentPath,
      cached: false,
      cacheWarning: e.message
    };
  }
}

async function syncFileCacheIfParentCached(env, key, metadata = {}) {
  try {
    await deleteDirectoryCache(env, parentPathFromR2Key(key));
  } catch (e) {
    console.warn('KV file cache sync failed:', e.message);
  }
}

async function syncFolderCacheIfParentCached(env, key) {
  try {
    const path = r2KeyToPath(key);
    await deleteDirectoryCache(env, parentPathFromItemPath(path));
  } catch (e) {
    console.warn('KV folder cache sync failed:', e.message);
  }
}

/**
 * 检查文件是否可预览
 */
function getPreviewType(filename) {
  const ext = filename.split('.').pop().toLowerCase();
  
  // Image files
  if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp'].includes(ext)) {
    return 'image';
  }
  
  // PDF files
  if (ext === 'pdf') {
    return 'pdf';
  }
  
  // Text/code files
  if (['txt', 'md', 'json', 'js', 'ts', 'css', 'html', 'xml', 'yaml', 'yml', 'ini', 'conf', 'sh', 'bash', 'py', 'java', 'c', 'cpp', 'h', 'hpp', 'go', 'rs', 'sql', 'log'].includes(ext)) {
    return 'text';
  }
  
  // Word documents (use Mammoth.js)
  if (ext === 'docx') {
    return 'word';
  }
  
  // Video files
  if (['mp4', 'webm', 'ogg'].includes(ext)) {
    return 'video';
  }
  
  // Audio files
  if (['mp3', 'wav', 'ogg', 'flac', 'm4a'].includes(ext)) {
    return 'audio';
  }
  
  return null;
}

/**
 * Parse cookies from request
 */
function parseCookies(request) {
  const cookieHeader = request.headers.get('Cookie') || '';
  const cookies = {};
  cookieHeader.split(';').forEach(cookie => {
    const [name, value] = cookie.trim().split('=');
    if (name && value) {
      cookies[name] = decodeURIComponent(value);
    }
  });
  return cookies;
}

/**
 * Create JSON response
 */
function jsonResponse(data, status = 200, headers = {}) {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      'Content-Type': 'application/json',
      ...headers
    }
  });
}

/**
 * Create HTML response
 */
function htmlResponse(html, status = 200, headers = {}) {
  return new Response(html, {
    status,
    headers: {
      'Content-Type': 'text/html; charset=utf-8',
      ...headers
    }
  });
}

// ============================================================================
// 身份验证处理程序
// ============================================================================

async function handleLogin(request, env) {
  try {
    const body = await request.json();
    const { email, password, isAdmin } = body;
    
    if (isAdmin) {
      // Admin login
      if (password === env.ADMIN_PASSWORD) {
        const token = await createJWT(
          { role: 'admin', exp: Date.now() + 24 * 60 * 60 * 1000 },
          env.ADMIN_PASSWORD
        );
        return jsonResponse(
          { success: true, role: 'admin' },
          200,
          { 'Set-Cookie': `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400` }
        );
      }
      return jsonResponse({ success: false, message: '管理员密码错误' }, 401);
    } else {
      // User login
      if (!email || !password) {
        return jsonResponse({ success: false, message: '请输入邮箱和密码' }, 400);
      }
      
      const userData = await env.KV_STORE.get(`user:${email}`);
      if (!userData) {
        return jsonResponse({ success: false, message: '用户不存在' }, 401);
      }
      
      const user = JSON.parse(userData);
      const passwordHash = await hashPassword(password);
      
      if (user.passwordHash !== passwordHash) {
        return jsonResponse({ success: false, message: '密码错误' }, 401);
      }
      
      const token = await createJWT(
        { email: user.email, role: 'user', exp: Date.now() + 24 * 60 * 60 * 1000 },
        env.ADMIN_PASSWORD
      );
      
      return jsonResponse(
        { success: true, role: 'user', email: user.email },
        200,
        { 'Set-Cookie': `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400` }
      );
    }
  } catch (e) {
    return jsonResponse({ success: false, message: '登录失败: ' + e.message }, 500);
  }
}

async function handleLogout() {
  return jsonResponse(
    { success: true },
    200,
    { 'Set-Cookie': 'token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0' }
  );
}

async function verifyAuth(request, env) {
  const cookies = parseCookies(request);
  const token = cookies.token;
  
  if (!token) return null;
  
  return await verifyJWT(token, env.ADMIN_PASSWORD);
}

async function requireAuth(request, env) {
  const auth = await verifyAuth(request, env);
  if (!auth) {
    return jsonResponse({ success: false, message: '未授权' }, 401);
  }
  return auth;
}

async function requireAdmin(request, env) {
  const auth = await verifyAuth(request, env);
  if (!auth || auth.role !== 'admin') {
    return jsonResponse({ success: false, message: '需要管理员权限' }, 403);
  }
  return auth;
}

// ============================================================================
// FILE MANAGEMENT HANDLERS
// ============================================================================

async function handleListFiles(request, env, path) {
  const auth = await requireAuth(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const currentPath = normalizeDirectoryPath(path);
    let cached = null;
    try {
      cached = await readDirectoryCache(env, currentPath);
    } catch (cacheError) {
      console.warn('KV directory cache read failed:', cacheError.message);
    }

    if (cached) {
      return jsonResponse(cached);
    }

    const fresh = await refreshDirectoryCache(env, currentPath);
    return jsonResponse(fresh);
  } catch (e) {
    return jsonResponse({ success: false, message: '获取文件列表失败: ' + e.message }, 500);
  }
}

async function handleRefreshDirectoryCache(request, env) {
  const auth = await requireAuth(request, env);
  if (auth instanceof Response) return auth;

  try {
    const body = await request.json().catch(() => ({}));
    const currentPath = normalizeDirectoryPath(body.path || '/');
    const refreshed = await refreshDirectoryCache(env, currentPath);
    return jsonResponse(refreshed);
  } catch (e) {
    return jsonResponse({ success: false, message: '刷新缓存失败: ' + e.message }, 500);
  }
}

async function handleUploadFile(request, env, path) {
  const auth = await requireAuth(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const formData = await request.formData();
    const file = formData.get('file');
    
    if (!file) {
      return jsonResponse({ success: false, message: '没有上传文件' }, 400);
    }
    
    // Normalize path
    let filePath = path || '';
    if (filePath.startsWith('/')) filePath = filePath.slice(1);
    if (filePath && !filePath.endsWith('/')) filePath += '/';
    
    const key = filePath + file.name;
    
    await env.R2_BUCKET.put(key, file.stream(), {
      httpMetadata: { contentType: file.type || getMimeType(file.name) }
    });

    await syncFileCacheIfParentCached(env, key, {
      size: file.size || 0,
      contentType: file.type || getMimeType(file.name),
      lastModified: new Date().toISOString()
    });
    
    return jsonResponse({ success: true, message: '文件上传成功', path: '/' + key });
  } catch (e) {
    return jsonResponse({ success: false, message: '文件上传失败: ' + e.message }, 500);
  }
}

async function handleDeleteFile(request, env, path) {
  const auth = await requireAuth(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    let key = path || '';
    if (key.startsWith('/')) key = key.slice(1);
    
    // Check if it's a folder (has objects with this prefix)
    const listed = await env.R2_BUCKET.list({ prefix: key + '/', limit: 1 });
    
    if (listed.objects && listed.objects.length > 0) {
      // It's a folder, delete all contents recursively
      let cursor;
      do {
        const batch = await env.R2_BUCKET.list({ prefix: key + '/', cursor });
        if (batch.objects && batch.objects.length > 0) {
          await env.R2_BUCKET.delete(batch.objects.map(obj => obj.key));
        }
        cursor = batch.truncated ? batch.cursor : null;
      } while (cursor);
    }
    
    // Try to delete the file itself
    await env.R2_BUCKET.delete(key);
    await invalidateCachePath(env, r2KeyToPath(key));
    
    return jsonResponse({ success: true, message: '删除成功' });
  } catch (e) {
    return jsonResponse({ success: false, message: '删除失败: ' + e.message }, 500);
  }
}

async function handleRenameFile(request, env, path) {
  const auth = await requireAuth(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const body = await request.json();
    const { newName } = body;
    
    if (!newName) {
      return jsonResponse({ success: false, message: '请提供新名称' }, 400);
    }
    
    let oldKey = path || '';
    if (oldKey.startsWith('/')) oldKey = oldKey.slice(1);
    
    const parentPath = oldKey.includes('/') ? oldKey.substring(0, oldKey.lastIndexOf('/') + 1) : '';
    const newKey = parentPath + newName;
    
    // Get the old file
    const oldObject = await env.R2_BUCKET.get(oldKey);
    if (oldObject) {
      // Copy to new location
      await env.R2_BUCKET.put(newKey, oldObject.body, {
        httpMetadata: oldObject.httpMetadata
      });

      // Delete old file
      await env.R2_BUCKET.delete(oldKey);
      await invalidateCachePath(env, r2KeyToPath(oldKey));

      const newObject = await env.R2_BUCKET.head(newKey);
      await syncFileCacheIfParentCached(env, newKey, {
        size: newObject?.size || oldObject.size || 0,
        contentType: newObject?.httpMetadata?.contentType || oldObject.httpMetadata?.contentType || getMimeType(newName),
        lastModified: isoDateString(newObject?.uploaded || new Date())
      });

      return jsonResponse({ success: true, message: '重命名成功', newPath: '/' + newKey });
    }

    const oldPrefix = oldKey.endsWith('/') ? oldKey : oldKey + '/';
    const folderCheck = await env.R2_BUCKET.list({ prefix: oldPrefix, limit: 1 });
    if (!folderCheck.objects || folderCheck.objects.length === 0) {
      return jsonResponse({ success: false, message: '文件不存在' }, 404);
    }

    const newPrefix = newKey.endsWith('/') ? newKey : newKey + '/';
    let cursor;
    do {
      const batch = await env.R2_BUCKET.list({ prefix: oldPrefix, cursor });
      const oldKeys = [];

      if (batch.objects && batch.objects.length > 0) {
        for (const obj of batch.objects) {
          const targetKey = newPrefix + obj.key.slice(oldPrefix.length);
          const source = await env.R2_BUCKET.get(obj.key);
          if (source) {
            await env.R2_BUCKET.put(targetKey, source.body, {
              httpMetadata: source.httpMetadata
            });
            oldKeys.push(obj.key);
          }
        }
      }

      if (oldKeys.length > 0) {
        await env.R2_BUCKET.delete(oldKeys);
      }

      cursor = batch.truncated ? batch.cursor : null;
    } while (cursor);

    await invalidateCachePath(env, r2KeyToPath(oldKey));
    await syncFolderCacheIfParentCached(env, newKey);

    return jsonResponse({ success: true, message: '重命名成功', newPath: '/' + newKey });
  } catch (e) {
    return jsonResponse({ success: false, message: '重命名失败: ' + e.message }, 500);
  }
}

async function handleCreateFolder(request, env) {
  const auth = await requireAuth(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const body = await request.json();
    let { path: folderPath } = body;
    
    if (!folderPath) {
      return jsonResponse({ success: false, message: '请提供文件夹路径' }, 400);
    }
    
    if (folderPath.startsWith('/')) folderPath = folderPath.slice(1);
    if (!folderPath.endsWith('/')) folderPath += '/';
    
    // Create an empty placeholder file to represent the folder
    await env.R2_BUCKET.put(folderPath + '.folder', new Uint8Array(0));
    await syncFolderCacheIfParentCached(env, folderPath.slice(0, -1));
    
    return jsonResponse({ success: true, message: '文件夹创建成功', path: '/' + folderPath.slice(0, -1) });
  } catch (e) {
    return jsonResponse({ success: false, message: '创建文件夹失败: ' + e.message }, 500);
  }
}

async function handleDownloadFile(request, env, path) {
  const auth = await verifyAuth(request, env);
  if (!auth) {
    return jsonResponse({ success: false, message: '未授权' }, 401);
  }
  
  try {
    let key = path || '';
    if (key.startsWith('/')) key = key.slice(1);
    
    const object = await env.R2_BUCKET.get(key);
    if (!object) {
      return jsonResponse({ success: false, message: '文件不存在' }, 404);
    }
    
    const filename = key.split('/').pop();
    
    return new Response(object.body, {
      headers: {
        'Content-Type': object.httpMetadata?.contentType || getMimeType(filename),
        'Content-Disposition': createAttachmentDisposition(filename),
        'Content-Length': object.size
      }
    });
  } catch (e) {
    return jsonResponse({ success: false, message: '下载失败: ' + e.message }, 500);
  }
}

// Preview file handler - returns file content for inline viewing
async function handlePreviewFile(request, env, path) {
  const auth = await verifyAuth(request, env);
  if (!auth) {
    return jsonResponse({ success: false, message: '未授权' }, 401);
  }
  
  try {
    let key = path || '';
    if (key.startsWith('/')) key = key.slice(1);
    
    const object = await env.R2_BUCKET.get(key, {
      range: request.headers
    });
    if (!object) {
      return jsonResponse({ success: false, message: '文件不存在' }, 404);
    }
    
    const filename = key.split('/').pop();
    const contentType = object.httpMetadata?.contentType || getMimeType(filename);
    const headers = new Headers({
      'Content-Type': contentType,
      'Content-Disposition': createInlineDisposition(filename),
      'Cache-Control': 'private, max-age=3600',
      'Accept-Ranges': 'bytes'
    });

    if (object.httpEtag) {
      headers.set('ETag', object.httpEtag);
    }

    if (object.range && typeof object.range.offset === 'number') {
      const end = object.range.end ?? object.size - 1;
      headers.set('Content-Range', `bytes ${object.range.offset}-${end}/${object.size}`);
      headers.set('Content-Length', String(end - object.range.offset + 1));
      return new Response(object.body, {
        status: 206,
        headers
      });
    }

    headers.set('Content-Length', String(object.size));
    
    return new Response(object.body, {
      headers
    });
  } catch (e) {
    return jsonResponse({ success: false, message: '预览失败: ' + e.message }, 500);
  }
}

// ============================================================================
// 共享处理程序
// ============================================================================

async function handleCreateShare(request, env) {
  const auth = await requireAuth(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const body = await request.json();
    const { filePath, password, expiresIn } = body;
    
    if (!filePath) {
      return jsonResponse({ success: false, message: '请提供文件路径' }, 400);
    }
    
    // Verify file exists
    let key = filePath;
    if (key.startsWith('/')) key = key.slice(1);
    
    const object = await env.R2_BUCKET.head(key);
    if (!object) {
      return jsonResponse({ success: false, message: '文件不存在' }, 404);
    }
    
    const shareId = generateId(12);
    const shareData = {
      shareId,
      filePath: key,
      fileName: key.split('/').pop(),
      fileSize: object.size,
      passwordHash: password ? await hashPassword(password) : null,
      expiresAt: getExpirationTime(expiresIn || '1d'),
      viewCount: 0,
      downloadCount: 0,
      createdAt: Date.now()
    };
    
    await env.KV_STORE.put(`share:${shareId}`, JSON.stringify(shareData));
    
    // Update stats
    const totalShares = parseInt(await env.KV_STORE.get('stats:totalShares') || '0');
    await env.KV_STORE.put('stats:totalShares', String(totalShares + 1));
    
    return jsonResponse({
      success: true,
      shareId,
      shareUrl: `/s/${shareId}`
    });
  } catch (e) {
    return jsonResponse({ success: false, message: '创建分享链接失败: ' + e.message }, 500);
  }
}

async function handleGetShareInfo(request, env, shareId) {
  try {
    const shareData = await env.KV_STORE.get(`share:${shareId}`);
    if (!shareData) {
      return jsonResponse({ success: false, message: '分享链接不存在' }, 404);
    }
    
    const share = JSON.parse(shareData);
    
    // Check expiration
    if (share.expiresAt && Date.now() > share.expiresAt) {
      return jsonResponse({ success: false, message: '分享链接已过期' }, 410);
    }
    
    // Update view count
    share.viewCount++;
    await env.KV_STORE.put(`share:${shareId}`, JSON.stringify(share));
    
    // Update global stats
    const totalViews = parseInt(await env.KV_STORE.get('stats:totalViews') || '0');
    await env.KV_STORE.put('stats:totalViews', String(totalViews + 1));
    
    return jsonResponse({
      success: true,
      fileName: share.fileName,
      fileSize: share.fileSize,
      fileSizeFormatted: formatFileSize(share.fileSize),
      requiresPassword: !!share.passwordHash,
      expiresAt: share.expiresAt
    });
  } catch (e) {
    return jsonResponse({ success: false, message: '获取分享信息失败: ' + e.message }, 500);
  }
}

async function handleShareDownload(request, env, shareId) {
  try {
    const shareData = await env.KV_STORE.get(`share:${shareId}`);
    if (!shareData) {
      return jsonResponse({ success: false, message: '分享链接不存在' }, 404);
    }
    
    const share = JSON.parse(shareData);
    
    // Check expiration
    if (share.expiresAt && Date.now() > share.expiresAt) {
      return jsonResponse({ success: false, message: '分享链接已过期' }, 410);
    }
    
    // Check password
    if (share.passwordHash) {
      const body = await request.json();
      const { password } = body;
      
      if (!password) {
        return jsonResponse({ success: false, message: '请输入密码' }, 401);
      }
      
      const passwordHash = await hashPassword(password);
      if (passwordHash !== share.passwordHash) {
        return jsonResponse({ success: false, message: '密码错误' }, 401);
      }
    }
    
    // Get file from R2
    const object = await env.R2_BUCKET.get(share.filePath);
    if (!object) {
      return jsonResponse({ success: false, message: '文件不存在' }, 404);
    }
    
    // Update download count
    share.downloadCount++;
    await env.KV_STORE.put(`share:${shareId}`, JSON.stringify(share));
    
    // Update global stats
    const totalDownloads = parseInt(await env.KV_STORE.get('stats:totalDownloads') || '0');
    await env.KV_STORE.put('stats:totalDownloads', String(totalDownloads + 1));
    
    return new Response(object.body, {
      headers: {
        'Content-Type': object.httpMetadata?.contentType || getMimeType(share.fileName),
        'Content-Disposition': createAttachmentDisposition(share.fileName),
        'Content-Length': object.size
      }
    });
  } catch (e) {
    return jsonResponse({ success: false, message: '下载失败: ' + e.message }, 500);
  }
}

// ============================================================================
// 管理员处理程序
// ============================================================================

async function handleGetStats(request, env) {
  const auth = await requireAdmin(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const totalShares = parseInt(await env.KV_STORE.get('stats:totalShares') || '0');
    const totalViews = parseInt(await env.KV_STORE.get('stats:totalViews') || '0');
    const totalDownloads = parseInt(await env.KV_STORE.get('stats:totalDownloads') || '0');
    
    return jsonResponse({
      success: true,
      totalShares,
      totalViews,
      totalDownloads
    });
  } catch (e) {
    return jsonResponse({ success: false, message: '获取统计数据失败: ' + e.message }, 500);
  }
}

async function handleListShares(request, env) {
  const auth = await requireAdmin(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const shares = [];
    let cursor;
    
    do {
      const listed = await env.KV_STORE.list({ prefix: 'share:', cursor });
      for (const key of listed.keys) {
        const data = await env.KV_STORE.get(key.name);
        if (data) {
          const share = JSON.parse(data);
          shares.push({
            ...share,
            fileSizeFormatted: formatFileSize(share.fileSize),
            isExpired: share.expiresAt && Date.now() > share.expiresAt
          });
        }
      }
      cursor = listed.list_complete ? null : listed.cursor;
    } while (cursor);
    
    // Sort by creation date, newest first
    shares.sort((a, b) => b.createdAt - a.createdAt);
    
    return jsonResponse({ success: true, shares });
  } catch (e) {
    return jsonResponse({ success: false, message: '获取分享列表失败: ' + e.message }, 500);
  }
}

async function handleDeleteShare(request, env, shareId) {
  const auth = await requireAdmin(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    await env.KV_STORE.delete(`share:${shareId}`);
    
    // Update stats
    const totalShares = parseInt(await env.KV_STORE.get('stats:totalShares') || '0');
    if (totalShares > 0) {
      await env.KV_STORE.put('stats:totalShares', String(totalShares - 1));
    }
    
    return jsonResponse({ success: true, message: '分享链接已删除' });
  } catch (e) {
    return jsonResponse({ success: false, message: '删除分享链接失败: ' + e.message }, 500);
  }
}

async function handleListUsers(request, env) {
  const auth = await requireAdmin(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const users = [];
    let cursor;
    
    do {
      const listed = await env.KV_STORE.list({ prefix: 'user:', cursor });
      for (const key of listed.keys) {
        const data = await env.KV_STORE.get(key.name);
        if (data) {
          const user = JSON.parse(data);
          users.push({
            email: user.email,
            role: user.role,
            createdAt: user.createdAt
          });
        }
      }
      cursor = listed.list_complete ? null : listed.cursor;
    } while (cursor);
    
    return jsonResponse({ success: true, users });
  } catch (e) {
    return jsonResponse({ success: false, message: '获取用户列表失败: ' + e.message }, 500);
  }
}

async function handleCreateUser(request, env) {
  const auth = await requireAdmin(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const body = await request.json();
    const { email, password } = body;
    
    if (!email || !password) {
      return jsonResponse({ success: false, message: '请提供邮箱和密码' }, 400);
    }
    
    // Check if user already exists
    const existing = await env.KV_STORE.get(`user:${email}`);
    if (existing) {
      return jsonResponse({ success: false, message: '用户已存在' }, 409);
    }
    
    const userData = {
      email,
      passwordHash: await hashPassword(password),
      role: 'user',
      createdAt: Date.now()
    };
    
    await env.KV_STORE.put(`user:${email}`, JSON.stringify(userData));
    
    return jsonResponse({ success: true, message: '用户创建成功', email });
  } catch (e) {
    return jsonResponse({ success: false, message: '创建用户失败: ' + e.message }, 500);
  }
}

async function handleDeleteUser(request, env, email) {
  const auth = await requireAdmin(request, env);
  if (auth instanceof Response) return auth;
  
  try {
    const decodedEmail = decodeURIComponent(email);
    await env.KV_STORE.delete(`user:${decodedEmail}`);
    
    return jsonResponse({ success: true, message: '用户已删除' });
  } catch (e) {
    return jsonResponse({ success: false, message: '删除用户失败: ' + e.message }, 500);
  }
}

async function handleCheckAuth(request, env) {
  const auth = await verifyAuth(request, env);
  if (!auth) {
    return jsonResponse({ authenticated: false });
  }
  return jsonResponse({ authenticated: true, role: auth.role, email: auth.email });
}

// ============================================================================
// HTML页面
// ============================================================================

const CSS_STYLES = `
<style>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  
  :root {
    --primary: #6366f1;
    --primary-dark: #4f46e5;
    --primary-light: #818cf8;
    --secondary: #8b5cf6;
    --accent: #06b6d4;
    --background: #0f172a;
    --surface: #1e293b;
    --surface-light: #334155;
    --text: #f8fafc;
    --text-muted: #94a3b8;
    --success: #10b981;
    --warning: #f59e0b;
    --error: #ef4444;
    --gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%);
  }
  
  body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--background);
    color: var(--text);
    min-height: 100vh;
    line-height: 1.6;
  }
  
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }
  
  /* Buttons */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 20px;
    border: none;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;
    text-decoration: none;
  }
  
  .btn-primary {
    background: var(--gradient);
    color: white;
  }
  
  .btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3);
  }
  
  .btn-secondary {
    background: var(--surface-light);
    color: var(--text);
  }
  
  .btn-secondary:hover {
    background: var(--surface);
  }
  
  .btn-danger {
    background: var(--error);
    color: white;
  }
  
  .btn-danger:hover {
    background: #dc2626;
  }
  
  .btn-sm {
    padding: 6px 12px;
    font-size: 12px;
  }
  
  /* Forms */
  .form-group {
    margin-bottom: 20px;
  }
  
  .form-label {
    display: block;
    margin-bottom: 8px;
    font-weight: 500;
    color: var(--text-muted);
  }
  
  .form-input {
    width: 100%;
    padding: 12px 16px;
    background: var(--surface);
    border: 1px solid var(--surface-light);
    border-radius: 8px;
    color: var(--text);
    font-size: 14px;
    transition: all 0.2s ease;
  }
  
  .form-input:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
  }
  
  .form-select {
    width: 100%;
    padding: 12px 16px;
    background: var(--surface);
    border: 1px solid var(--surface-light);
    border-radius: 8px;
    color: var(--text);
    font-size: 14px;
    cursor: pointer;
  }
  
  /* Cards */
  .card {
    background: var(--surface);
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }
  
  .card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
  }
  
  .card-title {
    font-size: 18px;
    font-weight: 600;
  }
  
  /* Tables */
  .table-container {
    overflow-x: auto;
  }
  
  table {
    width: 100%;
    border-collapse: collapse;
  }
  
  th, td {
    padding: 12px 16px;
    text-align: left;
    border-bottom: 1px solid var(--surface-light);
  }
  
  th {
    font-weight: 600;
    color: var(--text-muted);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }
  
  tr:hover {
    background: var(--surface-light);
  }
  
  /* Modal */
  .modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.7);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease;
  }
  
  .modal-overlay.active {
    opacity: 1;
    visibility: visible;
  }
  
  .modal {
    background: var(--surface);
    border-radius: 16px;
    padding: 24px;
    width: 90%;
    max-width: 500px;
    transform: scale(0.9);
    transition: all 0.3s ease;
    max-height: 90vh;
    overflow-y: auto;
  }
  
  .modal-overlay.active .modal {
    transform: scale(1);
  }
  
  .modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
  }
  
  .modal-title {
    font-size: 20px;
    font-weight: 600;
  }
  
  .modal-close {
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 24px;
    cursor: pointer;
    padding: 0;
    line-height: 1;
  }
  
  .modal-close:hover {
    color: var(--text);
  }
  
  /* Preview Modal - Full Screen */
  .preview-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.95);
    display: flex;
    flex-direction: column;
    z-index: 2000;
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease;
  }
  
  .preview-overlay.active {
    opacity: 1;
    visibility: visible;
  }
  
  .preview-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 24px;
    background: var(--surface);
    border-bottom: 1px solid var(--surface-light);
  }
  
  .preview-filename {
    font-weight: 600;
    color: var(--text);
  }
  
  .preview-actions {
    display: flex;
    gap: 12px;
  }
  
  .preview-content {
    flex: 1;
    overflow: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
  }
  
  .preview-image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
  }
  
  .preview-text {
    width: 100%;
    height: 100%;
    background: var(--surface);
    border-radius: 8px;
    padding: 20px;
    overflow: auto;
    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
    font-size: 14px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-wrap: break-word;
  }
  
  .preview-pdf {
    width: 100%;
    height: 100%;
    border: none;
    border-radius: 8px;
  }
  
  .preview-video, .preview-audio {
    max-width: 100%;
    max-height: 100%;
  }
  
  .preview-markdown {
    width: 100%;
    max-width: 900px;
    height: 100%;
    background: var(--surface);
    border-radius: 8px;
    padding: 40px;
    overflow: auto;
    line-height: 1.8;
  }
  
  .preview-markdown h1, .preview-markdown h2, .preview-markdown h3 {
    margin-top: 24px;
    margin-bottom: 16px;
    color: var(--text);
  }
  
  .preview-markdown p {
    margin-bottom: 16px;
  }
  
  .preview-markdown code {
    background: var(--background);
    padding: 2px 6px;
    border-radius: 4px;
    font-family: 'Monaco', 'Menlo', monospace;
  }
  
  .preview-markdown pre {
    background: var(--background);
    padding: 16px;
    border-radius: 8px;
    overflow-x: auto;
    margin-bottom: 16px;
  }
  
  .preview-markdown pre code {
    background: none;
    padding: 0;
  }
  
  .preview-markdown blockquote {
    border-left: 4px solid var(--primary);
    padding-left: 16px;
    margin: 16px 0;
    color: var(--text-muted);
  }
  
  .preview-markdown ul, .preview-markdown ol {
    margin-bottom: 16px;
    padding-left: 24px;
  }
  
  .preview-markdown li {
    margin-bottom: 8px;
  }
  
  .preview-markdown a {
    color: var(--primary);
  }
  
  .preview-markdown img {
    max-width: 100%;
    border-radius: 8px;
  }
  
  .preview-markdown table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 16px;
  }
  
  .preview-markdown th, .preview-markdown td {
    border: 1px solid var(--surface-light);
    padding: 8px 12px;
  }
  
  .preview-office {
    width: 100%;
    height: 100%;
    background: white;
    border-radius: 8px;
  }
  
  .preview-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    color: var(--text-muted);
  }
  
  .preview-error {
    text-align: center;
    color: var(--error);
  }
  
  /* Toast */
  .toast-container {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 3000;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  
  .toast {
    padding: 16px 20px;
    border-radius: 8px;
    color: white;
    font-weight: 500;
    animation: slideIn 0.3s ease;
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 300px;
  }
  
  .toast-success {
    background: var(--success);
  }
  
  .toast-error {
    background: var(--error);
  }
  
  .toast-info {
    background: var(--primary);
  }
  
  @keyframes slideIn {
    from {
      transform: translateX(100%);
      opacity: 0;
    }
    to {
      transform: translateX(0);
      opacity: 1;
    }
  }
  
  /* Header */
  .header {
    background: var(--surface);
    padding: 16px 24px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-bottom: 1px solid var(--surface-light);
  }
  
  .logo {
    font-size: 24px;
    font-weight: 700;
    background: var(--gradient);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
  }
  
  .header-actions {
    display: flex;
    gap: 12px;
  }
  
  /* Breadcrumb */
  .breadcrumb {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 16px 0;
    flex-wrap: wrap;
  }
  
  .breadcrumb-item {
    color: var(--text-muted);
    text-decoration: none;
    transition: color 0.2s;
  }
  
  .breadcrumb-item:hover {
    color: var(--primary);
  }
  
  .breadcrumb-item.active {
    color: var(--text);
  }
  
  .breadcrumb-separator {
    color: var(--text-muted);
  }
  
  /* File List */
  .file-grid {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
    
  .file-item {
    display: flex;
    align-items: center;
    padding: 12px 16px;
    background: var(--surface-light);
    border-radius: 8px;
    transition: background 0.2s;
    gap: 12px;
    width: 100%;
    box-sizing: border-box;
  }
    
  .file-item:hover {
    background: var(--surface);
  }
    
  .file-icon {
    font-size: 24px;
    width: 32px;
    text-align: center;
    flex-shrink: 0;
  }
    
  .file-name {
    font-weight: 500;
    color: var(--text);
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    margin-right: 12px;
  }
    
  .file-meta {
    color: var(--text-muted);
    font-size: 0.875rem;
    width: 120px;
    text-align: right;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
  }
    
  .file-actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;
    flex-wrap: wrap;
    justify-content: flex-end;
  }
    
  .file-actions .btn {
    padding: 4px 10px;
    font-size: 0.75rem;
    white-space: nowrap;
  }

  @media (max-width: 768px) {
    .file-item {
      flex-wrap: wrap;
    }
      
    .file-meta {
      width: auto;
      order: 3;
      flex-basis: 100%;
      justify-content: flex-start;
      margin-top: 4px;
      margin-left: 44px; /* Align with text */
    }
      
    .file-actions {
      order: 4;
      flex-basis: 100%;
      margin-top: 8px;
      margin-left: 44px;
    }
  }
  
  /* Stats Cards */
  .stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 20px;
    margin-bottom: 30px;
  }
  
  .stat-card {
    background: var(--surface);
    border-radius: 16px;
    padding: 24px;
    text-align: center;
  }
  
  .stat-value {
    font-size: 36px;
    font-weight: 700;
    background: var(--gradient);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
  }
  
  .stat-label {
    color: var(--text-muted);
    font-size: 14px;
    margin-top: 8px;
  }
  
  /* Tabs */
  .tabs {
    display: flex;
    gap: 4px;
    background: var(--surface);
    padding: 4px;
    border-radius: 12px;
    margin-bottom: 24px;
  }
  
  .tab {
    flex: 1;
    padding: 12px 20px;
    border: none;
    background: transparent;
    color: var(--text-muted);
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    border-radius: 8px;
    transition: all 0.2s ease;
  }
  
  .tab.active {
    background: var(--primary);
    color: white;
  }
  
  .tab:hover:not(.active) {
    color: var(--text);
  }
  
  .tab-content {
    display: none;
  }
  
  .tab-content.active {
    display: block;
  }
  
  /* Badge */
  .badge {
    display: inline-block;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 500;
  }
  
  .badge-success {
    background: rgba(16, 185, 129, 0.2);
    color: var(--success);
  }
  
  .badge-warning {
    background: rgba(245, 158, 11, 0.2);
    color: var(--warning);
  }
  
  .badge-error {
    background: rgba(239, 68, 68, 0.2);
    color: var(--error);
  }
  
  .badge-info {
    background: rgba(99, 102, 241, 0.2);
    color: var(--primary);
  }
  
  /* Login Page */
  .login-container {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--background);
    padding: 20px;
  }
  
  .login-card {
    background: var(--surface);
    border-radius: 24px;
    padding: 40px;
    width: 100%;
    max-width: 460px;
    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
  }
  
  .login-header {
    text-align: center;
    margin-bottom: 32px;
  }
  
  .login-logo {
    font-size: 32px;
    font-weight: 700;
    background: var(--gradient);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    margin-bottom: 8px;
  }
  
  .login-subtitle {
    color: var(--text-muted);
  }
  
  .login-tabs {
    display: flex;
    gap: 4px;
    background: var(--background);
    padding: 4px;
    border-radius: 12px;
    margin-bottom: 24px;
  }
  
  .login-tab {
    flex: 1;
    padding: 12px;
    border: none;
    background: transparent;
    color: var(--text-muted);
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    border-radius: 8px;
    transition: all 0.2s ease;
  }
  
  .login-tab.active {
    background: var(--primary);
    color: white;
  }
  
  /* Share Page */
  .share-container {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--background);
    padding: 20px;
  }
  
  .share-card {
    background: var(--surface);
    border-radius: 24px;
    padding: 40px;
    width: 100%;
    max-width: 480px;
    text-align: center;
  }
  
  .share-icon {
    font-size: 64px;
    margin-bottom: 20px;
  }
  
  .share-filename {
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 8px;
    word-break: break-all;
  }
  
  .share-filesize {
    color: var(--text-muted);
    margin-bottom: 24px;
  }
  
  .share-expired {
    color: var(--error);
    font-size: 18px;
  }
  
  /* Empty State */
  .empty-state {
    text-align: center;
    padding: 60px 20px;
    color: var(--text-muted);
  }
  
  .empty-icon {
    font-size: 64px;
    margin-bottom: 16px;
    opacity: 0.5;
  }
  
  /* Responsive */
  @media (max-width: 768px) {
    .header {
      flex-direction: column;
      gap: 16px;
    }
    
    .header-actions {
      width: 100%;
      justify-content: center;
    }
    
    .file-grid {
      grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    }
    
    .stats-grid {
      grid-template-columns: 1fr;
    }
    
    .tabs {
      flex-direction: column;
    }
    
    .preview-header {
      flex-direction: column;
      gap: 12px;
    }
  }
  
  /* Loading Spinner */
  .spinner {
    width: 40px;
    height: 40px;
    border: 3px solid var(--surface-light);
    border-top-color: var(--primary);
    border-radius: 50%;
    animation: spin 1s linear infinite;
  }
  
  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }
  
  .loading-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(15, 23, 42, 0.8);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 3000;
  }
  
  /* Context Menu */
  .context-menu {
    position: fixed;
    background: var(--surface);
    border-radius: 8px;
    padding: 8px 0;
    min-width: 160px;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
    z-index: 1500;
    display: none;
  }
  
  .context-menu.active {
    display: block;
  }
  
  .context-menu-item {
    padding: 10px 16px;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 10px;
    transition: background 0.2s;
  }
  
  .context-menu-item:hover {
    background: var(--surface-light);
  }
  
  .context-menu-item.danger {
    color: var(--error);
  }
  
  /* Toolbar */
  .toolbar {
    display: flex;
    gap: 12px;
    margin-bottom: 20px;
    flex-wrap: wrap;
  }
  
  /* Upload Area */
  .upload-area {
    border: 2px dashed var(--surface-light);
    border-radius: 12px;
    padding: 40px;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s ease;
  }
  
  .upload-area:hover, .upload-area.dragover {
    border-color: var(--primary);
    background: rgba(99, 102, 241, 0.1);
  }
  
  .upload-area input {
    display: none;
  }
</style>
`;

const LOGIN_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>登录 - ws01图床</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
</head>
<body>
  <div class="login-container">
    <div class="login-card">
      <div class="login-header">
        <div class="login-logo">ws01图床</div>
        <div class="logibtitle">基于 Cloudflare 的 R2 云盘服务</div>
      </div>
      
      <div class="login-tabs">
        <button class="login-tab active" onclick="switchLoginTab('admin')">管理员登录</button>
        <button class="login-tab" onclick="switchLoginTab('user')">用户登录</button>
      </div>
      
      <form id="loginForm" onsubmit="handleLogin(event)">
        <div id="emailField" class="form-group" style="display: none;">
          <label class="form-label">邮箱</label>
          <input type="l" id="email" class="form-input" placeholder="请输入邮箱">
        </div>
        
        <div class="form-group">
          <label class="form-label">密码</label>
          <input type="password" id="password" class="form-input" placeholder="请输入密码" required>
        </div>
        
        <button type="submit" class="btn btn-primary" style="width: 100%;">
          登录
        </button>
      </form>
    </div>
  </div>
  
  <div class="toast-container" id="toastContainer"></div>
  
   let isAdminLogin = true;
    
    function switchLoginTab(type) {
      isAdminLogin = type === 'admin';
      document.querySelectorAll('.login-tab').forEach((tab, index) => {
        tab.classList.toggle('active', (index === 0 && isAdminLogin) || (index === 1 && !isAdminLogin));
      });
      document.getElementById('emailField').style.display = isAdminLogin ? 'none' : 'block';
    }
    
    async function handleLogin(e) {
      e.preventDefault();
      
      const password = document.getElementById('password').value;
      const email = document.getElementById('email').value;
      
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            isAdmin: isAdminLogin,
            email: isAdminLogin ? undefined : email,
            password
          })
        });
        
        const data = await response.json();
        
        if (data.success) {
          showToast('登录成功', 'success');
          setTimeout(() => {
            window.location.href = '/';
          }, 500);
        } else {
          showToast(data.message || '登录失败', 'error');
        }
      } catch (error) {
        showToast('登录失败: ' + error.message, 'error');
      }
    }
    
    function showToast(message, type = 'info') {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast t type;
      toast.textContent = message;
      container.appendChild(toast);
      
      setTimeout(() => {
        toast.remove();
      }, 3000);
    }
  </script>
</body>
</html>
`;

const INDEX_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ws01图床 - 云盘</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <script src="https://cdn.jsdelivt/npm/[email protected]/mammoth.browser.min.js"></script>
</head>
<body>
  <div class="header">
    <div class="logo">ws01图床</div>
    <div class="header-actions">
      <button class="btn btn-secondary" onclick="window.location.href='/admin.html'">管理后台</button>
      <button class="btn btn-secondary" onclick="logout()">退出登录</button>
    </div>
  </div>
  
  <div class="container">
    <div class="breadcrumb" id="breadcrumb"></div>
    
    <div class="toolbar">
      <button class="btn btn-primary" onclick="showNewFolderModal()">
        📁 新建文件夹
      </button>
      <button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
        📤 上传文件
      </button>
      <input type="file" id="fileInput" multiple style="display: none;" onchange="handleFileUpload(event)">
    </div>
    
    <div class="card">
      <div id="fileList" class="file-grid"></div>
      <div id="emptyState" class="empty-state" style="display: none;">
        <div class="empon">📂</div>
        <div>此文件夹为空</div>
      </div>
    </div>
  </div>
  
  <!-- New Folder Modal -->
  <div class="modal-overlay" id="newFolderModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">新建文件夹</div>
        <button class="modal-close" onclick="closeModal('newFolderModal')">&times;</button>
      </div>
      <form onsubmit="createFolder(event)">
        <div class="form-group">
          <label class="form-label">文件夹名称label>
          <input type="text" id="folderName" class="form-input" placeholder="请输入文件夹名称" required>
        </div>
        <button type="submit" class="btn btn-primary" style="width: 100%;">创建</button>
      </form>
    </div>
  </div>
  
  <!-- Rename Modal -->
  <div class="modal-overlay" id="renameModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">重命名</div>
        <button class="modal-close" onclick="closeModal('renameModal')">utton>
      </div>
      <form onsubmit="renameFile(event)">
        <div class="form-group">
          <label class="form-label">新名称</label>
          <input type="text" id="newFileName" class="form-input" required>
        </div>
        <input type="hidden" id="renameFilePath">
        <button type="submit" class="btn btn-primary" style="width: 100%;">确认</button>
      </form>
    </div>
  </div>
  
  <!-- Share Modal -->
  <div class="modal-overlay" id="shareModal">
    <div class="modal">
  lass="modal-header">
        <div class="modal-title">创建分享链接</div>
        <button class="modal-close" onclick="closeModal('shareModal')">&times;</button>
      </div>
      <form onsubmit="createShare(event)">
        <div class="form-group">
          <label class="form-label">分享密码(留空则无密码)</label>
          <input type="text" id="sharePassword" class="form-input" placeholder="可选">
        </div>
        <div class="form-group">
          <label class="form-label">æ
          <select id="shareExpiry" class="form-select">
            <option value="1h">1小时</option>
            <option value="1d">1天</option>
            <option value="1m">1月</option>
            <option value="1y" selected>1年</option>
            <option value="3y">3年</option>
            <option value="permanent">永久有效</option>
          </select>
        </div>
        <input type="hidden" id="shareFilePath">
        <button type="submit" class="btn btn-primary" style="width: 100%;">创建分享链接</button>
      </form>
    </div>
  </div>
  
  <!-- Share Result iv class="modal-overlay" id="shareResultModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">分享链接已创建</div>
        <button class="modal-close" onclick="closeModal('shareResultModal')">&times;</button>
      </div>
      <div class="form-group">
        <label class="form-label">分享链接</label>
        <input type="text" id="shareResultUrl" class="form-input" readonly>
      </div>
      <button class="btn btn-primary" style="width: 100%;" onclipyShareLink()">复制链接</button>
    </div>
  </div>
  
  <!-- Preview Modal -->
  <div class="preview-overlay" id="previewOverlay">
    <div class="preview-header">
      <div class="preview-filename" id="previewFilename"></div>
      <div class="preview-actions">
        <button class="btn btn-primary" id="previewDownloadBtn">下载</button>
        <button class="btn btn-secondary" onclick="closePreview()">关闭</button>
      </div>
    </div>
    <div class="preview-content" id="previewContent">
      <div class="preview-loading">
        <div class="spinner"></div>
        <div>加载中...</div>
      </div>
    </div>
  </div>
  
  <div class="toast-container" id="toastContainer"></div>
  
  <div class="loading-overlay" id="loadingOverlay" style="display: none;">
    <div class="spinner"></div>
  </div>
  
  <script>
    let currentPath = '/';

    function encodePathForUrl(path) {
      if (!path || path === '/') return '/';
      return path.split('/').map((part, index) => {
        if (index === 0 && part === '') return '';
        return encodeURIComponent(part);
      }).join('/');
    }

    function apiFileUrl(prefix, path) {
      return prefix + encodePathForUrl(path);
    }
    
    async function checkAuth() {
      try {
        const response = await fetch('/api/auth/check');
        const data = await response.json();
     (!data.authenticated) {
          window.location.href = '/login.html';
        }
      } catch (error) {
        window.location.href = '/login.html';
      }
    }
    
    async function loadFiles() {
      showLoading(true);
      try {
        const response = await fetch(apiFileUrl('/api/files', currentPath));
        const data = await response.json();
        
        if (!data.success) {
          if (response.status === 401) {
            window.location.href = '/login.html';
            return;
          }
          throw new Error(data.message);
        }
        
        renderBreadcrumb();
        renderFiles(data.folders, data.files);
      } catch (error) {
        showToast('加载文件失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }
    
    function renderBreadcrumb() {
      const breadcrumb = document.getElementById('breadcrumb');
      const parts = currentPath.split('/').filter(p => p);
      
      let html = '<a href="#" class="breadcrumb-item" ogateTo(\\'/\\')">🏠 根目录</a>';
      
      let path = '';
      parts.forEach((part, index) => {
        path += '/' + part;
        const isLast = index === parts.length - 1;
        html += '<span class="breadcrumb-separator">/</span>';
        if (isLast) {
          html += '<span class="breadcrumb-item active">' + part + '</span>';
        } else {
          html += '<a href="#" class="breadcrumb-item" onclick="navigateTo(\\'' + path + '\\')">' + part + '</a>';
        }
      });
      
      mb.innerHTML = html;
    }
    
    function renderFiles(folders, files) {
      const fileList = document.getElementById('fileList');
      const emptyState = document.getElementById('emptyState');
      
      if (folders.length === 0 && files.length === 0) {
        fileList.innerHTML = '';
        emptyState.style.display = 'block';
        return;
      }
      
      emptyState.style.display = 'none';
      
      let html = '';
      
      // Render folders
      folders.forEach(folder => {
        html += \`
          <div class="file-item" ondblclick="navigateTo('\${folder.path}')">
            <div class="file-icon">📁</div>
            <div class="file-name">\${escapeHtml(folder.name)}</div>
            <div class="file-meta">文件夹</div>
            <div class="file-actions">
              <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); showRenameModal('\${folder.path}', '\${escapeHtml(folder.name)}')">重命名</button>
              <button class="btn btn-sm btn-k="event.stopPropagation(); deleteFile('\${folder.path}')">删除</button>
            </div>
          </div>
        \`;
      });
      
      // Render files
      files.forEach(file => {
        const icon = getFileIcon(file.name);
        const previewType = getPreviewType(file.name);
        
        html += \`
          <div class="file-item" onclick="handleFileClick('\${file.path}', '\${previewType || ''}', '\${escapeHtml(file.name)}')">
            <div class="file-icon">\${icon}</div>
            <div class="file-name" title="\${escapeHtml(file.name)}">\${escapeHtml(file.name)}</div>
            <div class="file-meta">\${formatFileSize(file.size)}</div>
            <div class="file-actions">
              \${previewType 
                ? \`<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); previewFile('\${file.path}', '\${previewType}', '\${escapeHtml(file.name)}')">预览</button>\` 
                : \`<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); downloadFile('\${file.path}')">下载</button>\`}
              <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); copyFileLink('\${file.path}')">复制链接</button>
              <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); showShareModal('\${file.path}')">分享</button>
              <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); showRenameModal('\${file.path}', '\${escapeHtml(file.name)}')">重命名</button>
              <button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); deleteFile('\${file.path}')">删除</button>
            </div>
          </div>
        \`;
      });
      
      fileList.innerHTML = html;
    }
    
    function handleFileClick(path, previewType, filename) {
      if (previewType) {
        previewFile(path, previewType, filename);
      } else {
        downloadFile(path);
      }
    }
    
    function getFileIcon(filename) {
      const ext = filename.split('.').pop().toLowerCase();
      const icons = {
    'pdf': '📕',
        'doc': '📘', 'docx': '📘',
        'xls': '📗', 'xlsx': '📗',
        'ppt': '📙', 'pptx': '📙',
        'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️', 'webp': '🖼️',
        'mp3': '🎵', 'wav': '🎵', 'flac': '🎵',
        'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬', 'mov': '🎬',
        'zip': '📦', 'rar': '📦', '7z': '📦', 'tar': '📦', 'gz': '📦',
        'js': '📜', 'ts': '📜', 'py': '📜', 'java': 📜', 'c': '📜',
        'html': '🌐', 'css': '🎨', 'json': '📋',
        'txt': '📄', 'md': '📝'
      };
      return icons[ext] || '📄';
    }
    
    function navigateTo(path) {
      currentPath = path;
      loadFiles();
    }
    
    // ========== Preview Functions ==========
    
    async function previewFile(path, previewType, filename) {
      const overlay = document.getElementById('previewOverlay');
      const content = document.getElementById('previewContent');
      const fiocument.getElementById('previewFilename');
      const downloadBtn = document.getElementById('previewDownloadBtn');
      
      filenameEl.textContent = filename;
      downloadBtn.onclick = () => downloadFile(path);
      
      // Show loading
      content.innerHTML = '<div class="preview-loading"><div class="spinner"></div><div>加载中...</div></div>';
      overlay.classList.add('active');
      
      try {
        const previewUrl = apiFileUrl('/api/preview', path);
        
        switch (previewType) {
    case 'image':
            content.innerHTML = '<img class="preview-image" src="' + previewUrl + '" alt="' + escapeHtml(filename) + '">';
            break;
            
          case 'pdf':
            content.innerHTML = '<iframe class="preview-pdf" src="' + previewUrl + '"></iframe>';
            break;
            
          case 'text':
            const textResponse = await fetch(previewUrl);
            const text = await textResponse.text();
            const ext = filename.split('.').pop().toLowerCase();
            
            if (ext === 'md') {
              // Render Markdown
              const htmlContent = marked.parse(text);
              content.innerHTML = '<div class="preview-markdown">' + htmlContent + '</div>';
            } else if (ext === 'json') {
              // Pretty print JSON
              try {
                const json = JSON.parse(text);
                content.innerHTML = '<pre class="preview-text">' + escapeHtml(JSON.stringify(json, null, 2)) + '</pre>';
              } catch {
                content.innerHTML = '<pre class="preview-text">' + escapeHtml(text) + '</pre>';
              }
            } else {
              content.innerHTML = '<pre class="preview-text">' + escapeHtml(text) + '</pre>';
            }
            break;
            
          case 'video':
            content.innerHTML = '<video class="preview-video" controls autoplay><source src="' + previewUrl + '"></video>';
            break;
            
          case 'audio':
            content.innerHTML = '<audio class="preview-audio" controls autoplay><source src="' + previewUrl + '"></audio>';
            break;
            
          case 'word':
            // Use Mammoth.js to convert docx to HTML
            const docxResponse = await fetch(previewUrl);
            const docxArrayBuffer = await docxResponse.arrayBuffer();
            const result = await mammoth.convertToHtml({ arrayBuffer: docxArrayBuffer });
            content.innerHTML = '<div class="preview-markdown">' + result.value + '</div>';
            break;
            
          default:
            content.innerHTML = '<div class="preview-error">不支持预览此文件类型</div>';
        }
      } catch (error) {
        content.innerHTML = '<div class="preview-error">预览加载失败: ' + escapeHtml(error.message) + '</div>';
      }
    }
    
    function closePreview() {
      const overlay = document.getElementById('previewOverlay');
      overlay.classList.remove('active');
      // Clear content to stop any playing media
      document.getElementById('previewContent').innerHTML = '';
    }
    
    // Close preview on Escape key
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        closePreview();
      }
    });
    
    // ========== File Operations ==========
    
    async function handleFileUpload(event) {
      const files = event.target.files;
      if (!files.length) return;
      
      showLoading(true);
      
      for (const file of files) {
        try {
          const formData = new FormData();
          formData.append('file', file);
          
          const response = await fetch(apiFileUrl('/api/files', currentPath), {
            method: 'POST',
            body: formData
          });
          
          const data = await response.json();
          
          if (data.success) {
            showToast('文件 ' + file.name + ' 上传成功', 'success');
          } else {
            showToast('文件 ' + file.name + ' 上传失败: ' + data.message, 'error');
          }
    tch (error) {
          showToast('文件 ' + file.name + ' 上传失败: ' + error.message, 'error');
        }
      }
      
      event.target.value = '';
      loadFiles();
    }
    
    function showNewFolderModal() {
      document.getElementById('folderName').value = '';
      document.getElementById('newFolderModal').classList.add('active');
    }
    
    async function createFolder(event) {
      event.preventDefault();
      const name = document.getElementById('folderName').value.trim();
     !name) {
        showToast('请输入文件夹名称', 'error');
        return;
      }
      
      showLoading(true);
      closeModal('newFolderModal');
      
      try {
        let folderPath = currentPath;
        if (!folderPath.endsWith('/')) folderPath += '/';
        folderPath += name;
        
        const response = await fetch('/api/folders', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ path: folderPath })
        });
        
        const data = await response.json();
        
        if (data.success) {
          showToast('文件夹创建成功', 'success');
          loadFiles();
        } else {
          showToast('创建失败: ' + data.message, 'error');
        }
      } catch (error) {
        showToast('创建失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }
    
    function showRenameModal(path, currentName) {
      document.getElementById('renameFilePath').v     document.getElementById('newFileName').value = currentName;
      document.getElementById('renameModal').classList.add('active');
    }
    
    async function renameFile(event) {
      event.preventDefault();
      const path = document.getElementById('renameFilePath').value;
      const newName = document.getElementById('newFileName').value.trim();
      
      if (!newName) {
        showToast('请输入新名称', 'error');
        return;
      }
      
      showLoading(true);
      closeModal('r;
      
      try {
        const response = await fetch(apiFileUrl('/api/files', path), {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ newName })
        });
        
        const data = await response.json();
        
        if (data.success) {
          showToast('重命名成功', 'success');
          loadFiles();
        } else {
          showToast('重命名失败: ' + data.message, 'error');
        }
      } catch (error) {
        shast('重命名失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }
    
    async function deleteFile(path) {
      if (!confirm('确定要删除吗?此操作不可恢复。')) return;
      
      showLoading(true);
      
      try {
        const response = await fetch(apiFileUrl('/api/files', path), {
          method: 'DELETE'
        });
        
        const data = await response.json();
        
        if (data.success) {
          showToast('删除成功', 'success');
          loadFiles();
        } else {
          showToast('删除失败: ' + data.message, 'error');
        }
      } catch (error) {
        showToast('删除失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }
    
    async function downloadFile(path) {
      window.open(apiFileUrl('/api/download', path), '_blank');
    }
    
    function showShareModal(path) {
      document.getElementById('shareFilePath').value = path;
      document.getElementById('sharePassword').value = '';
      document.getElementById('shareExpiry').value = '1d';
      document.getElementById('shareModal').classList.add('active');
    }
    
    async function createShare(event) {
      event.preventDefault();
      const filePath = document.getElementById('shareFilePath').value;
      const password = document.getElementById('sharePassword').value;
      const expiresIn = document.getElementById('shareExpiry').value;
      
      showLoading(true);
      closeModal('shareModal');
      
      try {
        const response = await fetch('/api/share', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ filePath, password, expiresIn })
        });
        
        const data = await response.json();
        
        if (data.success) {
          const fullUrl = window.location.origin + data.shareUrl;
          document.getElementById('shareResultUrl').value = fullUrl;
          document.getElementById('shareResultModal').classList.add('active');
        } else {
          showToast('创建分享链接失败: ' + data.message, 'error');
        }
      } catch (error) {
        showToast('创建分享链接失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }
    
    function copyShareLink() {
      const input = document.getElementById('shareResultUrl');
      input.select();
      document.execCommand('copy');
      showToast('链接已复制到剪贴板', 'success');
    }
    
    asc function logout() {
      try {
        await fetch('/api/logout', { method: 'POST' });
        window.location.href = '/login.html';
      } catch (error) {
        window.location.href = '/login.html';
      }
    }
    
    function closeModal(id) {
      document.getElementById(id).classList.remove('active');
    }
    
    function showLoading(show) {
      document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
    }
    
    function showToast(message, type = 'info') {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast toast-' + type;
      toast.textContent = message;
      container.appendChild(toast);
      
      setTimeout(() => {
        toast.remove();
      }, 3000);
    }
    
    function escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    }
    
    // Initialize
    checkAuth();
    loadFiles();
  </script>
</body>
</html>
`;

const ADMIN_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>管理后台 - ws01图床</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
</head>
<body>
  <div class="header">
    <div class="logo">ws01图床 管理后台</div>
    <div class="header-actions">
      <button class="btn btn-secondary" onclick="window.location.href='/'">返回云盘</button>
      <button class="btn btn-secondlick="logout()">退出登录</button>
    </div>
  </div>
  
  <div class="container">
    <div class="tabs">
      <button class="tab active" onclick="switchTab('stats')">统计数据</button>
      <button class="tab" onclick="switchTab('shares')">分享链接</button>
      <button class="tab" onclick="switchTab('users')">授权用户</button>
    </div>
    
    <!-- Stats Tab -->
    <div id="statsTab" class="tab-content active">
      <div class="stats-grid">
        <div class="stat-card">
          <div class="stat-value" id="totalShares">0</div>
          <div class="stat-label">总分享链接数</div>
        </div>
        <div class="stat-card">
          <div class="stat-value" id="totalViews">0</div>
          <div class="stat-label">总浏览次数</div>
        </div>
        <div class="stat-card">
          <div class="stat-value" id="totalDownloads">0</div>
          <div class="stat-label">总下载次数</div>
        </div>
      </div>
    </div>
    
    <!-- Shares Tab -->
    <div id="sharesTab" class="tab-content">
      <div class="card">
        <div class="card-header">
          <div class="card-title">分享链接管理</div>
        </div>
        <div class="table-container">
          <table>
            <thead>
              <tr>
                <th>文件名</th>
                <th>分享ID</th>
                <th>密码保护</th>
                <th>浏览次数</th>
                <th>下载次数</th>
                <th>状态</th>
                <th>操作</th>
        </tr>
            </thead>
            <tbody id="sharesTable"></tbody>
          </table>
        </div>
      </div>
    </div>
    
    <!-- Users Tab -->
    <div id="usersTab" class="tab-content">
      <div class="card">
        <div class="card-header">
          <div class="card-title">授权用户管理</div>
          <button class="btn btn-primary" onclick="showAddUserModal()">添加用户</button>
        </div>
        <div class="table-container">
          <table>
            <thead>
          <tr>
                <th>邮箱</th>
                <th>角色</th>
                <th>创建时间</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody id="usersTable"></tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
  
  <!-- Add User Modal -->
  <div class="modal-overlay" id="addUserModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">添加授权用户</div>
        <button class="modal-close" onclick="closeModal('addUserModal')">&times;</button>
      </div>
      <form onsubmit="addUser(event)">
        <div class="form-group">
          <label class="form-label">邮箱</label>
          <input type="email" id="newUserEmail" class="form-input" placeholder="请输入邮箱" required>
        </div>
        <div class="form-group">
          <label class="form-label">密码</label>
          <input type="text" id="newUserPassword" class="form-input" placeholder="请输入密码" re    </div>
        <button type="submit" class="btn btn-primary" style="width: 100%;">添加用户</button>
      </form>
    </div>
  </div>
  
  <div class="toast-container" id="toastContainer"></div>
  
  <div class="loading-overlay" id="loadingOverlay" style="display: none;">
    <div class="spinner"></div>
  </div>
  
  <script>
    async function checkAdminAuth() {
      try {
        const response = await fetch('/api/auth/check');
        const data = await response.json();
        if (!data.authent| data.role !== 'admin') {
          window.location.href = '/login.html';
        }
      } catch (error) {
        window.location.href = '/login.html';
      }
    }
    
    function switchTab(tab) {
      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
      document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
      
      event.target.classList.add('active');
      document.getElementById(tab + 'Tab').classList.add('active');
      
      if (tab === 'stats') loadStats();
      else if (tab === 'shares') loadShares();
      else if (tab === 'users') loadUsers();
    }
    
    async function loadStats() {
      try {
        const response = await fetch('/api/admin/stats');
        const data = await response.json();
        
        if (data.success) {
          document.getElementById('totalShares').textContent = data.totalShares;
          document.getElementById('totalViews').textContent = data.totalViews;
          document.getElementById('totalDownloads').textContent = data.totalDownloads;
        }
      } catch (error) {
        showToast('加载统计数据失败', 'error');
      }
    }
    
    async function loadShares() {
      showLoading(true);
      try {
        const response = await fetch('/api/admin/shares');
        const data = await response.json();
        
        if (data.success) {
          const tbody = document.getElementById('sharesTable');
          
          if (data.shares.length === 0) {
            tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">暂无分享链接</td></tr>';
            return;
          }
          
          tbody.innerHTML = data.shares.map(share => \`
            <tr>
              <td>\${escapeHtml(share.fileName)}</td>
              <td><code>\${share.shareId}</code></td>
              <td>\${share.passwordHash ? '是' : '否'}</td>
              <td>\${share.viewCount}</td>
              <td>\${share.downloadCount}</td>
              <td>
                \${share.isExpired 
                  ? '<span class="badge badge-error">已过期</span>' 
                  : '<span class="badge badge-success">有效</span>'}
              </td>
              <td>
                <button class="btn btn-sm btn-secondary" onclick="copyShareLink('\${share.shareId}')">复制链接</button>
                <button class="btn btn-sm btn-danger" onclick="deleteShare('\${share.shareId}')">删除</button>
              </td>
            </tr>
          \`).join(       }
      } catch (error) {
        showToast('加载分享列表失败', 'error');
      } finally {
        showLoading(false);
      }
    }
    
    async function loadUsers() {
      showLoading(true);
      try {
        const response = await fetch('/api/admin/users');
        const data = await response.json();
        
        if (data.success) {
          const tbody = document.getElementById('usersTable');
          
          if (data.users.length === 0) {
            tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: var(--text-muted);">暂无授权用户</td></tr>';
            return;
          }
          
          tbody.innerHTML = data.users.map(user => \`
            <tr>
              <td>\${escapeHtml(user.email)}</td>
              <td>\${user.role === 'admin' ? '管理员' : '普通用户'}</td>
              <td>\${user.createdAt ? new Date(user.createdAt).toLocaleString() : '-'}</td>
              <td>
                <button class="btn btn-sm btn-danger" oleteUser('\${encodeURIComponent(user.email)}')">撤销授权</button>
              </td>
            </tr>
          \`).join('');
        }
      } catch (error) {
        showToast('加载用户列表失败', 'error');
      } finally {
        showLoading(false);
      }
    }
    
    function showAddUserModal() {
      document.getElementById('newUserEmail').value = '';
      document.getElementById('newUserPassword').value = '';
      document.getElementById('addUserModal').classList.add('active');
  
    async function addUser(event) {
      event.preventDefault();
      const email = document.getElementById('newUserEmail').value;
      const password = document.getElementById('newUserPassword').value;
      
      showLoading(true);
      closeModal('addUserModal');
      
      try {
        const response = await fetch('/api/admin/users', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email, password })
        });
        
        const data = await response.json();
        
        if (data.success) {
          showToast('用户添加成功', 'success');
          loadUsers();
        } else {
          showToast('添加失败: ' + data.message, 'error');
        }
      } catch (error) {
        showToast('添加失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }
    
    async function deleteUser(email) {
      if (!confirm('确定要撤销该用户的授权吗?')) return;
       showLoading(true);
      
      try {
        const response = await fetch('/api/admin/users/' + email, {
          method: 'DELETE'
        });
        
        const data = await response.json();
        
        if (data.success) {
          showToast('用户已删除', 'success');
          loadUsers();
        } else {
          showToast('删除失败: ' + data.message, 'error');
        }
      } catch (error) {
        showToast('删除失败: ' + error.message, 'error');
      } finally {
       ng(false);
      }
    }
    
    async function deleteShare(shareId) {
      if (!confirm('确定要删除该分享链接吗?')) return;
      
      showLoading(true);
      
      try {
        const response = await fetch('/api/admin/shares/' + shareId, {
          method: 'DELETE'
        });
        
        const data = await response.json();
        
        if (data.success) {
          showToast('分享链接已删除', 'success');
          loadShares();
        } else {
          showToast('å data.message, 'error');
        }
      } catch (error) {
        showToast('删除失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }
    
    function copyShareLink(shareId) {
      const url = window.location.origin + '/s/' + shareId;
      navigator.clipboard.writeText(url).then(() => {
        showToast('链接已复制', 'success');
      }).catch(() => {
        showToast('复制失败', 'error');
      });
    }
    
    async function logout() {
          await fetch('/api/logout', { method: 'POST' });
        window.location.href = '/login.html';
      } catch (error) {
        window.location.href = '/login.html';
      }
    }
    
    function closeModal(id) {
      document.getElementById(id).classList.remove('active');
    }
    
    function showLoading(show) {
      document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
    }
    
    function showToast(message, type = 'info') {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast toast-' + type;
      toast.textContent = message;
      container.appendChild(toast);
      
      setTimeout(() => {
        toast.remove();
      }, 3000);
    }
    
    function escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    }
    
    // Initialize
    checkAdminAuth();
    loadStats();
  </script>
</body>
</html>
`;

const SHARE_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件分享 - ws01图床</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
</head>
<body>
  <div class="share-container">
    <div class="share-card" id="shareCard">
      <div id="loadingState">
        <div class="spinner" style="margin: 0 auto 20px;"></div>
        <div>加载中...</div>
      </div>
      
      <div id="expiredState" style="display: noneiv class="share-icon">⚠️</div>
        <div class="share-expired">分享链接已过期或不存在</div>
        <p style="color: var(--text-muted); margin-top: 16px;">请联系分享者获取新的链接</p>
      </div>
      
      <div id="shareContent" style="display: none;">
        <div class="share-icon">📄</div>
        <div class="share-filename" id="fileName"></div>
        <div class="share-filesize" id="fileSize"></div>
        
        <div id="passwordForm" style="display: none;">
      <div class="form-group">
            <label class="form-label">请输入分享密码</label>
            <input type="password" id="sharePassword" class="form-input" placeholder="输入密码">
          </div>
        </div>
        
        <button class="btn btn-primary" style="width: 100%; margin-top: 20px;" onclick="downloadFile()">
          下载文件
        </button>
      </div>
    </div>
  </div>
  
  <div class="toast-container" id="toastContainer"></div>
  
  <script>
    let shareId = '';resPassword = false;
    
    async function loadShareInfo() {
      // Get share ID from URL
      const pathParts = window.location.pathname.split('/');
      shareId = pathParts[pathParts.length - 1];
      
      if (!shareId) {
        showExpired();
        return;
      }
      
      try {
        const response = await fetch('/api/share/' + shareId);
        const data = await response.json();
        
        if (!data.success) {
          showExpired();
          return;
        }
        
        document.getElementById('loadingState').style.display = 'none';
        document.getElementById('shareContent').style.display = 'block';
        
        document.getElementById('fileName').textContent = data.fileName;
        document.getElementById('fileSize').textContent = data.fileSizeFormatted;
        
        requiresPassword = data.requiresPassword;
        if (requiresPassword) {
          document.getElementById('passwordForm').style.display = 'block';
        }
      } catch (error) {
        showExpired();
      }
    }
    
    function showExpired() {
      document.getElementById('loadingState').style.display = 'none';
      document.getElementById('expiredState').style.display = 'block';
    }
    
    async function downloadFile() {
      const password = document.getElementById('sharePassword')?.value || '';
      
      if (requiresPassword && !password) {
        showToast('请输入分享密码', 'error');
        return;
      }
      
      try {
        const response = await fetch(' shareId + '/download', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ password })
        });
        
        if (response.ok) {
          // Get filename from Content-Disposition header
          const contentDisposition = response.headers.get('Content-Disposition');
          let filename = 'download';
          if (contentDisposition) {
            const utf8Match = contentDisposition.match(/filename\\*=UTF-8''([^;\\n]+)/i);
            const fallbackMatch = contentDisposition.match(/filename=["']?([^"';\\n]+)/i);
            if (utf8Match) {
              filename = decodeURIComponent(utf8Match[1]);
            } else if (fallbackMatch) {
              filename = fallbackMatch[1];
            }
          }
          
          // Download the file
          const blob = await response.blob();
          const url = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.download = filename;
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
          URL.revokeObjectURL(url);
          
          showToast('下载开始', 'success');
        } else {
          const data = await response.json();
          showToast(data.message || '下载失败', 'error');
        }
      } catch (error) {
        showToast('下载失败: ' + error.message, 'error');
      }
    }
    
    function showToast(message, type = 'info') {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast toast-' + type;
      toast.textContesage;
      container.appendChild(toast);
      
      setTimeout(() => {
        toast.remove();
      }, 3000);
    }
    
    // Initialize
    loadShareInfo();
  </script>
</body>
</html>
`;

const FIXED_LOGIN_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>登录 - ws01图床</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
</head>
<body>
  <div class="login-container">
    <div class="login-card">
      <div class="login-header">
        <div class="login-logo">ws01图床</div>
        <div class="login-subtitle">基于 Cloudflare 的 R2 云盘服务</div>
      </div>

      <div class="login-tabs">
        <button type="button" class="login-tab active" onclick="switchLoginTab('admin')">管理员登录</button>
        <button type="button" class="login-tab" onclick="switchLoginTab('user')">用户登录</button>
      </div>

      <form id="loginForm" onsubmit="handleLogin(event)">
        <div id="emailField" class="form-group" style="display: none;">
          <label class="form-label" for="email">邮箱</label>
          <input type="email" id="email" class="form-input" placeholder="请输入邮箱">
        </div>

        <div class="form-group">
          <label class="form-label" for="password">密码</label>
          <input type="password" id="password" class="form-input" placeholder="请输入密码" required>
        </div>

        <button type="submit" class="btn btn-primary" style="width: 100%;">登录</button>
      </form>
    </div>
  </div>

  <div class="toast-container" id="toastContainer"></div>

  <script>
    let isAdminLogin = true;

    function switchLoginTab(type) {
      isAdminLogin = type === 'admin';
      const tabs = document.querySelectorAll('.login-tab');
      tabs[0].classList.toggle('active', isAdminLogin);
      tabs[1].classList.toggle('active', !isAdminLogin);
      document.getElementById('emailField').style.display = isAdminLogin ? 'none' : 'block';
    }

    async function handleLogin(event) {
      event.preventDefault();

      const password = document.getElementById('password').value;
      const email = document.getElementById('email').value.trim();

      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            isAdmin: isAdminLogin,
            email: isAdminLogin ? undefined : email,
            password
          })
        });

        const data = await response.json();
        if (data.success) {
          showToast('登录成功', 'success');
          window.setTimeout(function () {
            window.location.href = '/';
          }, 300);
        } else {
          showToast(data.message || '登录失败', 'error');
        }
      } catch (error) {
        showToast('登录失败: ' + error.message, 'error');
      }
    }

    function showToast(message, type) {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast toast-' + (type || 'info');
      toast.textContent = message;
      container.appendChild(toast);
      window.setTimeout(function () {
        toast.remove();
      }, 3000);
    }
  </script>
</body>
</html>
`;

const FIXED_INDEX_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ws01图床 - 云盘</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
  <style>
    /* 新增/修改样式以支持缩略图 */
    .file-thumbnail {
      width: 32px;
      height: 32px;
      object-fit: cover;
      border-radius: 4px;
      flex-shrink: 0;
      background-color: var(--surface);
    }
    
    .file-icon {
      font-size: 24px;
      width: 32px;
      text-align: center;
      flex-shrink: 0;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    /* 分页样式 */
    .pagination {
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 8px;
      margin-top: 20px;
      padding-top: 20px;
      border-top: 1px solid var(--surface-light);
    }

    .pagination-btn {
      padding: 6px 12px;
      background: var(--surface-light);
      border: 1px solid transparent;
      border-radius: 6px;
      color: var(--text);
      cursor: pointer;
      transition: all 0.2s;
      font-size: 14px;
    }

    .pagination-btn:hover:not(:disabled) {
      background: var(--primary);
      color: white;
    }

    .pagination-btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    .pagination-btn.active {
      background: var(--primary);
      color: white;
      border-color: var(--primary);
    }

    .pagination-info {
      color: var(--text-muted);
      font-size: 14px;
      margin: 0 10px;
    }

    /* 编辑器样式 */
    .editor-textarea {
      width: 100%;
      min-height: 400px;
      background: var(--background);
      color: var(--text);
      border: 1px solid var(--surface-light);
      border-radius: 8px;
      padding: 12px;
      font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
      font-size: 14px;
      line-height: 1.5;
      resize: vertical;
    }
    .editor-textarea:focus {
      outline: none;
      border-color: var(--primary);
    }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/mammoth.browser.min.js"></script>
</head>
<body>
  <div class="header">
    <div class="logo">ws01图床</div>
    <div class="header-actions">
      <button type="button" class="btn btn-secondary" onclick="refreshCurrentDirectory()">刷新</button>
      <button type="button" class="btn btn-secondary" onclick="window.location.href='/admin.html'">管理后台</button>
      <button type="button" class="btn btn-secondary" onclick="logout()">退出登录</button>
    </div>
  </div>

  <div class="container">
    <div class="breadcrumb" id="breadcrumb"></div>

    <div class="toolbar">
      <button type="button" class="btn btn-primary" onclick="showNewFolderModal()">📁 新建文件夹</button>
      <button type="button" class="btn btn-primary" onclick="document.getElementById('fileInput').click()">📤 上传文件</button>
      <input type="file" id="fileInput" multiple style="display: none;" onchange="handleFileUpload(event)">
    </div>

    <div class="card">
      <div id="fileList" class="file-grid"></div>
      <div id="emptyState" class="empty-state" style="display: none;">
        <div class="empty-icon">📂</div>
        <div>此文件夹为空</div>
      </div>
      <!-- 分页控件 -->
      <div id="pagination" class="pagination" style="display: none;"></div>
    </div>
  </div>

  <div class="modal-overlay" id="newFolderModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">新建文件夹</div>
        <button type="button" class="modal-close" onclick="closeModal('newFolderModal')">&times;</button>
      </div>
      <form onsubmit="createFolder(event)">
        <div class="form-group">
          <label class="form-label" for="folderName">文件夹名称</label>
          <input type="text" id="folderName" class="form-input" placeholder="请输入文件夹名称" required>
        </div>
        <button type="submit" class="btn btn-primary" style="width: 100%;">创建</button>
      </form>
    </div>
  </div>

  <div class="modal-overlay" id="renameModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">重命名</div>
        <button type="button" class="modal-close" onclick="closeModal('renameModal')">&times;</button>
      </div>
      <form onsubmit="renameFile(event)">
        <div class="form-group">
          <label class="form-label" for="newFileName">新名称</label>
          <input type="text" id="newFileName" class="form-input" required>
        </div>
        <input type="hidden" id="renameFilePath">
        <button type="submit" class="btn btn-primary" style="width: 100%;">确认</button>
      </form>
    </div>
  </div>

  <div class="modal-overlay" id="shareModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">创建分享链接</div>
        <button type="button" class="modal-close" onclick="closeModal('shareModal')">&times;</button>
      </div>
      <form onsubmit="createShare(event)">
        <div class="form-group">
          <label class="form-label" for="sharePassword">分享密码(留空则无密码)</label>
          <input type="text" id="sharePassword" class="form-input" placeholder="可选">
        </div>
        <div class="form-group">
          <label class="form-label" for="shareExpiry">有效期</label>
          <select id="shareExpiry" class="form-select">
            <option value="1h">1小时</option>
            <option value="1d">1天</option>
            <option value="1m">1月</option>
            <option value="1y" selected>1年</option>
            <option value="3y">3年</option>
            <option value="permanent">永久有效</option>
          </select>
        </div>
        <input type="hidden" id="shareFilePath">
        <button type="submit" class="btn btn-primary" style="width: 100%;">创建分享链接</button>
      </form>
    </div>
  </div>

  <div class="modal-overlay" id="shareResultModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">分享链接已创建</div>
        <button type="button" class="modal-close" onclick="closeModal('shareResultModal')">&times;</button>
      </div>
      <div class="form-group">
        <label class="form-label" for="shareResultUrl">分享链接</label>
        <input type="text" id="shareResultUrl" class="form-input" readonly>
      </div>
      <button type="button" class="btn btn-primary" style="width: 100%;" onclick="copyShareLink()">复制链接</button>
    </div>
  </div>

  <!-- Edit File Modal -->
  <div class="modal-overlay" id="editFileModal">
    <div class="modal" style="max-width: 800px;">
      <div class="modal-header">
        <div class="modal-title">编辑文件</div>
        <button type="button" class="modal-close" onclick="closeModal('editFileModal')">&times;</button>
      </div>
      <form onsubmit="saveEditedFile(event)">
        <div class="form-group">
          <textarea id="fileContentEditor" class="editor-textarea" required></textarea>
        </div>
        <input type="hidden" id="editFilePath">
        <input type="hidden" id="editFileType">
        <div style="display: flex; gap: 10px;">
          <button type="submit" class="btn btn-primary" style="flex: 1;">保存</button>
          <button type="button" class="btn btn-secondary" style="flex: 1;" onclick="closeModal('editFileModal')">取消</button>
        </div>
      </form>
    </div>
  </div>

  <div class="preview-overlay" id="previewOverlay">
    <div class="preview-header">
      <div class="preview-filename" id="previewFilename"></div>
      <div class="preview-actions">
        <button type="button" class="btn btn-primary" id="previewDownloadBtn">下载</button>
        <button type="button" class="btn btn-secondary" onclick="closePreview()">关闭</button>
      </div>
    </div>
    <div class="preview-content" id="previewContent"></div>
  </div>

  <div class="toast-container" id="toastContainer"></div>
  <div class="loading-overlay" id="loadingOverlay" style="display: none;"><div class="spinner"></div></div>

  <script>
    let currentPath = '/';
    // 分页相关变量
    let currentPage = 1;
    const pageSize = 20;
    let allFolders = [];
    let allFiles = [];

    function encodePathForUrl(path) {
      if (!path || path === '/') return '/';
      return path.split('/').map(function (part, index) {
        if (index === 0 && part === '') return '';
        return encodeURIComponent(part);
      }).join('/');
    }

    function apiFileUrl(prefix, path) {
      return prefix + encodePathForUrl(path);
    }

    async function checkAuth() {
      try {
        const response = await fetch('/api/auth/check');
        const data = await response.json();
        if (!data.authenticated) {
          window.location.href = '/login.html';
        }
      } catch (error) {
        window.location.href = '/login.html';
      }
    }

    async function loadFiles() {
      showLoading(true);
      try {
        const response = await fetch(apiFileUrl('/api/files', currentPath));
        const data = await response.json();
        if (!data.success) {
          if (response.status === 401) {
            window.location.href = '/login.html';
            return;
          }
          throw new Error(data.message || '加载失败');
        }
        currentPath = data.currentPath || currentPath;
        // 保存所有数据并重置页码
        allFolders = data.folders || [];
        allFiles = data.files || [];
        currentPage = 1;
        
        renderBreadcrumb();
        renderFiles();
      } catch (error) {
        showToast('加载文件失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    async function refreshCurrentDirectory() {
      showLoading(true);
      try {
        const response = await fetch('/api/cache/refresh', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ path: currentPath })
        });
        const data = await response.json();
        if (!data.success) {
          throw new Error(data.message || '刷新失败');
        }
        currentPath = data.currentPath || currentPath;
        renderBreadcrumb();
        renderFiles(data.folders || [], data.files || []);
        showToast('已刷新当前目录', 'success');
      } catch (error) {
        showToast('刷新失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    function renderBreadcrumb() {
      const breadcrumb = document.getElementById('breadcrumb');
      breadcrumb.replaceChildren();

      const root = document.createElement('a');
      root.href = '#';
      root.className = 'breadcrumb-item';
      root.textContent = '根目录';
      root.addEventListener('click', function (event) {
        event.preventDefault();
        navigateTo('/');
      });
      breadcrumb.appendChild(root);

      let path = '';
      currentPath.split('/').filter(Boolean).forEach(function (part, index, parts) {
        const separator = document.createElement('span');
        separator.className = 'breadcrumb-separator';
        separator.textContent = '/';
        breadcrumb.appendChild(separator);

        path += '/' + part;
        if (index === parts.length - 1) {
          const active = document.createElement('span');
          active.className = 'breadcrumb-item active';
          active.textContent = part;
          breadcrumb.appendChild(active);
        } else {
          const link = document.createElement('a');
          link.href = '#';
          link.className = 'breadcrumb-item';
          link.textContent = part;
          const targetPath = path;
          link.addEventListener('click', function (event) {
            event.preventDefault();
            navigateTo(targetPath);
          });
          breadcrumb.appendChild(link);
        }
      });
    }

    function renderFiles() {
      const fileList = document.getElementById('fileList');
      const emptyState = document.getElementById('emptyState');
      const paginationEl = document.getElementById('pagination');
      
      fileList.replaceChildren();

      const totalItems = allFolders.length + allFiles.length;

      if (totalItems === 0) {
        emptyState.style.display = 'block';
        paginationEl.style.display = 'none';
        return;
      }

      emptyState.style.display = 'none';
      
      // 计算分页
      const totalPages = Math.ceil(totalItems / pageSize);
      // 确保当前页不超过总页数
      if (currentPage > totalPages && totalPages > 0) {
        currentPage = totalPages;
      }
      
      const startIndex = (currentPage - 1) * pageSize;
      const endIndex = startIndex + pageSize;
      
      // 合并文件夹和文件进行切片,或者分别处理?
      // 通常文件夹排在前面。这里我们假设文件夹始终在最前面显示,不分页混合,或者整体分页。
      // 需求是“每页显示20个文件”,通常包含文件夹。
      // 策略:将 folders 和 files 视为一个连续列表进行分页。
      // 更简单的策略:文件夹永远在第一页或者单独处理?
      // 为了简单且符合直觉:我们将 folders 和 files 视为一个连续列表进行分页。
      
      const combinedList = [
        ...allFolders.map(f => ({ ...f, itemType: 'folder' })),
        ...allFiles.map(f => ({ ...f, itemType: 'file' }))
      ];
      
      const pageItems = combinedList.slice(startIndex, endIndex);
      
      // 渲染当前页的项目
      pageItems.forEach(item => {
        if (item.itemType === 'folder') {
          fileList.appendChild(createFileCard({
            name: item.name,
            path: item.path,
            typeLabel: '📁',
            meta: '文件夹',
            isFolder: true
          }));
        } else {
          fileList.appendChild(createFileCard({
            name: item.name,
            path: item.path,
            typeLabel: getFileIcon(item.name),
            meta: item.sizeFormatted || '',
            previewType: item.previewType || '',
            isFolder: false
          }));
        }
      });

      // 渲染分页控件
      renderPagination(totalPages, totalItems);
    }

    function renderPagination(totalPages, totalItems) {
      const paginationEl = document.getElementById('pagination');
      paginationEl.innerHTML = '';

      if (totalPages <= 1) {
        paginationEl.style.display = 'none';
        return;
      }

      paginationEl.style.display = 'flex';

      // 上一页按钮
      const prevBtn = document.createElement('button');
      prevBtn.className = 'pagination-btn';
      prevBtn.textContent = '上一页';
      prevBtn.disabled = currentPage === 1;
      prevBtn.onclick = () => changePage(currentPage - 1);
      paginationEl.appendChild(prevBtn);

      // 页码信息
      const infoSpan = document.createElement('span');
      infoSpan.className = 'pagination-info';
      infoSpan.textContent = \`第 \${currentPage} / \${totalPages} 页 (共 \${totalItems} 项)\`;
      paginationEl.appendChild(infoSpan);

      // 下一页按钮
      const nextBtn = document.createElement('button');
      nextBtn.className = 'pagination-btn';
      nextBtn.textContent = '下一页';
      nextBtn.disabled = currentPage === totalPages;
      nextBtn.onclick = () => changePage(currentPage + 1);
      paginationEl.appendChild(nextBtn);
    }

    function changePage(page) {
      if (page < 1) return;
      const totalPages = Math.ceil((allFolders.length + allFiles.length) / pageSize);
      if (page > totalPages) return;
      
      currentPage = page;
      renderFiles();
      // 滚动到列表顶部
      document.getElementById('fileList').scrollIntoView({ behavior: 'smooth', block: 'start' });
    }

    function createFileCard(item) {
      const card = document.createElement('div');
      card.className = 'file-item';
      card.addEventListener('dblclick', function () {
        if (item.isFolder) {
          navigateTo(item.path);
        } else {
          handleFileClick(item.path, item.previewType, item.name);
        }
      });

      // 判断是否为图片类型以显示缩略图
      const isImage = !item.isFolder && item.previewType === 'image';
      
      const iconContainer = document.createElement('div');
      iconContainer.className = isImage ? 'file-thumbnail-container' : 'file-icon';
      
      if (isImage) {
        const img = document.createElement('img');
        img.className = 'file-thumbnail';
        img.src = apiFileUrl('/api/preview', item.path);
        img.alt = item.name;
        img.loading = 'lazy'; // 懒加载优化性能
        iconContainer.appendChild(img);
        // 移除 file-icon class,因为我们要用 img 标签
        iconContainer.className = ''; 
        iconContainer.style.width = '32px';
        iconContainer.style.flexShrink = '0';
        iconContainer.appendChild(img);
      } else {
        iconContainer.className = 'file-icon';
        iconContainer.textContent = item.typeLabel;
      }
      
      card.appendChild(iconContainer);

      const name = document.createElement('div');
      name.className = 'file-name';
      name.textContent = item.name;
      card.appendChild(name);

      const meta = document.createElement('div');
      meta.className = 'file-meta';
      meta.textContent = item.meta;
      if (item.previewType) {
        const badge = document.createElement('span');
        badge.className = 'badge badge-info';
        badge.textContent = ' 可预览';
        meta.appendChild(badge);
      }
      card.appendChild(meta);

      const actions = document.createElement('div');
      actions.className = 'file-actions';
      if (item.isFolder) {
        actions.appendChild(createActionButton('打开', 'btn-primary', function () {
          navigateTo(item.path);
        }));
        actions.appendChild(createActionButton('删除', 'btn-danger', function () {
          deleteFile(item.path);
        }));
      } else {
        if (item.previewType) {
          actions.appendChild(createActionButton('预览', 'btn-primary', function () {
            previewFile(item.path, item.previewType, item.name);
          }));
        }
        
        // 添加编辑按钮用于 JSON 和 TXT
        const ext = item.name.split('.').pop().toLowerCase();
        if (ext === 'json' || ext === 'txt') {
          actions.appendChild(createActionButton('编辑', 'btn-secondary', function () {
            showEditModal(item.path, ext);
          }));
        }

        actions.appendChild(createActionButton('下载', 'btn-primary', function () {
          downloadFile(item.path);
        }));
        actions.appendChild(createActionButton('复制链接', 'btn-secondary', function () {
          copyFileLink(item.path);
        }));
        actions.appendChild(createActionButton('分享', 'btn-secondary', function () {
          showShareModal(item.path);
        }));
        actions.appendChild(createActionButton('重命名', 'btn-secondary', function () {
          showRenameModal(item.path, item.name);
        }));
        actions.appendChild(createActionButton('删除', 'btn-danger', function () {
          deleteFile(item.path);
        }));
      }
      card.appendChild(actions);
      return card;
    }

    function createActionButton(label, className, handler) {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'btn btn-sm ' + className;
      button.textContent = label;
      button.addEventListener('click', function (event) {
        event.stopPropagation();
        handler();
      });
      return button;
    }

    function getFileIcon(filename) {
      const ext = (filename.split('.').pop() || '').toLowerCase();
      const icons = {
        pdf: '📕',
        doc: '📘',
        docx: '📘',
        xls: '📗',
        xlsx: '📗',
        ppt: '📙',
        pptx: '📙',
        jpg: '🖼️',
        jpeg: '🖼️',
        png: '🖼️',
        gif: '🖼️',
        svg: '🖼️',
        webp: '🖼️',
        mp3: '🎵',
        wav: '🎵',
        flac: '🎵',
        m4a: '🎵',
        mp4: '🎬',
        webm: '🎬',
        zip: '📦',
        rar: '📦',
        '7z': '📦',
        tar: '📦',
        gz: '📦',
        txt: '📄',
        md: '📝',
        json: '📋',
        js: '📜',
        ts: '📜',
        css: '🎨',
        html: '🌐'
      };
      return icons[ext] || '📄';
    }

    function navigateTo(path) {
      currentPath = path || '/';
      loadFiles();
    }

    function handleFileClick(path, previewType, filename) {
      if (previewType) {
        previewFile(path, previewType, filename);
      } else {
        downloadFile(path);
      }
    }

    async function previewFile(path, previewType, filename) {
      const overlay = document.getElementById('previewOverlay');
      const content = document.getElementById('previewContent');
      const filenameEl = document.getElementById('previewFilename');
      const downloadBtn = document.getElementById('previewDownloadBtn');

      filenameEl.textContent = filename;
      downloadBtn.onclick = function () {
        downloadFile(path);
      };
      content.innerHTML = '<div class="preview-loading"><div class="spinner"></div><div>加载中...</div></div>';
      overlay.classList.add('active');

      const previewUrl = apiFileUrl('/api/preview', path);
      try {
        if (previewType === 'image') {
          const img = document.createElement('img');
          img.className = 'preview-image';
          img.src = previewUrl;
          img.alt = filename;
          content.replaceChildren(img);
        } else if (previewType === 'pdf') {
          const iframe = document.createElement('iframe');
          iframe.className = 'preview-pdf';
          iframe.src = previewUrl + '#toolbar=1';
          content.replaceChildren(iframe);
        } else if (previewType === 'video') {
          const video = document.createElement('video');
          video.className = 'preview-video';
          video.controls = true;
          video.autoplay = true;
          video.src = previewUrl;
          content.replaceChildren(video);
        } else if (previewType === 'audio') {
          const audio = document.createElement('audio');
          audio.className = 'preview-audio';
          audio.controls = true;
          audio.autoplay = true;
          audio.src = previewUrl;
          content.replaceChildren(audio);
        } else if (previewType === 'word') {
          if (!window.mammoth) throw new Error('文档预览组件加载失败');
          const response = await fetch(previewUrl);
          if (!response.ok) throw new Error('文件读取失败');
          const buffer = await response.arrayBuffer();
          const result = await window.mammoth.convertToHtml({ arrayBuffer: buffer });
          const wrapper = document.createElement('div');
          wrapper.className = 'preview-markdown';
          wrapper.innerHTML = result.value;
          content.replaceChildren(wrapper);
        } else if (previewType === 'text') {
          const response = await fetch(previewUrl);
          if (!response.ok) throw new Error('文件读取失败');
          const buffer = await response.arrayBuffer();
          const text = decodeTextBuffer(buffer);
          const ext = (filename.split('.').pop() || '').toLowerCase();
          if (ext === 'md' && window.marked) {
            const wrapper = document.createElement('div');
            wrapper.className = 'preview-markdown';
            wrapper.innerHTML = window.marked.parse(text);
            content.replaceChildren(wrapper);
          } else {
            const pre = document.createElement('pre');
            pre.className = 'preview-text';
            if (ext === 'json') {
              try {
                pre.textContent = JSON.stringify(JSON.parse(text), null, 2);
              } catch (error) {
                pre.textContent = text;
              }
            } else {
              pre.textContent = text;
            }
            content.replaceChildren(pre);
          }
        } else {
          showPreviewError('不支持预览此文件类型');
        }
      } catch (error) {
        showPreviewError('预览加载失败: ' + error.message);
      }
    }

    function decodeTextBuffer(buffer) {
      const bytes = new Uint8Array(buffer);
      if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
        return new TextDecoder('utf-8').decode(bytes.subarray(3));
      }
      if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
        return new TextDecoder('utf-16le').decode(bytes.subarray(2));
      }
      if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
        return new TextDecoder('utf-16be').decode(bytes.subarray(2));
      }

      try {
        return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
      } catch (error) {
        try {
          return new TextDecoder('gb18030').decode(bytes);
        } catch (gbError) {
          return new TextDecoder('utf-8').decode(bytes);
        }
      }
    }

    function showPreviewError(message) {
      const content = document.getElementById('previewContent');
      const error = document.createElement('div');
      error.className = 'preview-error';
      error.textContent = message;
      content.replaceChildren(error);
    }

    function closePreview() {
      document.getElementById('previewOverlay').classList.remove('active');
      document.getElementById('previewContent').replaceChildren();
    }

    document.addEventListener('keydown', function (event) {
      if (event.key === 'Escape') closePreview();
    });

    async function handleFileUpload(event) {
      const files = Array.from(event.target.files || []);
      if (files.length === 0) return;

      showLoading(true);
      for (const file of files) {
        try {
          const formData = new FormData();
          formData.append('file', file);
          const response = await fetch(apiFileUrl('/api/files', currentPath), {
            method: 'POST',
            body: formData
          });
          const data = await response.json();
          if (data.success) {
            showToast('文件 ' + file.name + ' 上传成功', 'success');
          } else {
            showToast('文件 ' + file.name + ' 上传失败: ' + (data.message || '未知错误'), 'error');
          }
        } catch (error) {
          showToast('文件 ' + file.name + ' 上传失败: ' + error.message, 'error');
        }
      }
      event.target.value = '';
      showLoading(false);
      loadFiles();
    }

    function showNewFolderModal() {
      document.getElementById('folderName').value = '';
      document.getElementById('newFolderModal').classList.add('active');
    }

    async function createFolder(event) {
      event.preventDefault();
      const name = document.getElementById('folderName').value.trim();
      if (!name) {
        showToast('请输入文件夹名称', 'error');
        return;
      }

      showLoading(true);
      closeModal('newFolderModal');
      try {
        let folderPath = currentPath;
        if (!folderPath.endsWith('/')) folderPath += '/';
        folderPath += name;
        const response = await fetch('/api/folders', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ path: folderPath })
        });
        const data = await response.json();
        if (data.success) {
          showToast('文件夹创建成功', 'success');
          loadFiles();
        } else {
          showToast('创建失败: ' + (data.message || '未知错误'), 'error');
        }
      } catch (error) {
        showToast('创建失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    function showRenameModal(path, currentName) {
      document.getElementById('renameFilePath').value = path;
      document.getElementById('newFileName').value = currentName;
      document.getElementById('renameModal').classList.add('active');
    }

    async function renameFile(event) {
      event.preventDefault();
      const path = document.getElementById('renameFilePath').value;
      const newName = document.getElementById('newFileName').value.trim();
      if (!newName) {
        showToast('请输入新名称', 'error');
        return;
      }

      showLoading(true);
      closeModal('renameModal');
      try {
        const response = await fetch(apiFileUrl('/api/files', path), {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ newName })
        });
        const data = await response.json();
        if (data.success) {
          showToast('重命名成功', 'success');
          loadFiles();
        } else {
          showToast('重命名失败: ' + (data.message || '未知错误'), 'error');
        }
      } catch (error) {
        showToast('重命名失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    async function deleteFile(path) {
      if (!window.confirm('确定要删除吗?此操作不可恢复。')) return;
      showLoading(true);
      try {
        const response = await fetch(apiFileUrl('/api/files', path), { method: 'DELETE' });
        const data = await response.json();
        if (data.success) {
          showToast('删除成功', 'success');
          loadFiles();
        } else {
          showToast('删除失败: ' + (data.message || '未知错误'), 'error');
        }
      } catch (error) {
        showToast('删除失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    function copyFileLink(filePath) {
      const url = window.location.origin + '/api/download' + filePath;
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(url).then(function () {
          showToast('链接已复制', 'success');
        }).catch(function () {
          showToast('复制失败', 'error');
        });
      } else {
        // Fallback for older browsers or non-HTTPS contexts
        const input = document.createElement('input');
        input.value = url;
        document.body.appendChild(input);
        input.select();
        document.execCommand('copy');
        document.body.removeChild(input);
        showToast('链接已复制', 'success');
      }
    }

    async function showEditModal(path, type) {
      showLoading(true);
      try {
        const response = await fetch(apiFileUrl('/api/preview', path));
        if (!response.ok) throw new Error('读取文件失败');
        const content = await response.text();
        
        document.getElementById('editFilePath').value = path;
        document.getElementById('editFileType').value = type;
        document.getElementById('fileContentEditor').value = content;
        document.getElementById('editFileModal').classList.add('active');
      } catch (error) {
        showToast('加载文件内容失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    async function saveEditedFile(event) {
      event.preventDefault();
      const path = document.getElementById('editFilePath').value;
      const type = document.getElementById('editFileType').value;
      const content = document.getElementById('fileContentEditor').value;
      
      showLoading(true);
      closeModal('editFileModal');
      
      try {
        // 构造 Blob 以便作为文件上传/更新
        const blob = new Blob([content], { type: type === 'json' ? 'application/json' : 'text/plain' });
        const formData = new FormData();
        // 提取文件名
        const fileName = path.split('/').pop();
        formData.append('file', blob, fileName);
        
        // 使用现有的上传接口,但需要注意上传接口通常是 POST 到目录
        // 这里的 rename/update 逻辑可能需要特殊处理,或者我们直接 PUT 到文件路径
        // 查看后端: handleUploadFile 是 POST. handleRenameFile 是 PUT 但只改名字
        // 我们需要一个更新文件内容的接口。
        // 由于后端没有专门的 update content 接口,我们可以复用 upload 逻辑如果它支持覆盖,
        // 或者更简单地,直接 PUT 到 /api/files/path 如果后端支持。
        // 检查后端: handleUploadFile 是 POST. handleRenameFile 是 PUT.
        // 让我们假设我们可以通过 DELETE 然后 POST 来更新,或者最好添加一个后端支持。
        // 但题目只要求修改前端代码文件?不,题目说“针对需求给出修改后的代码”,且提供了 _worker.js。
        // 我需要同时修改后端以支持内容更新,或者利用现有机制。
        // 现有机制中,POST /api/files/path 是上传新文件。如果文件名相同,R2 put 会覆盖。
        // 所以我们可以 POST 到当前文件所在的目录,文件名保持不变.
        
        const dirPath = path.substring(0, path.lastIndexOf('/'));
        const response = await fetch(apiFileUrl('/api/files', dirPath === '' ? '/' : dirPath), {
          method: 'POST',
          body: formData
        });
        
        const data = await response.json();
        if (data.success) {
          showToast('文件保存成功', 'success');
          loadFiles(); // 刷新列表以更新大小和时间
        } else {
          showToast('保存失败: ' + data.message, 'error');
        }
      } catch (error) {
        showToast('保存失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    async function downloadFile(filePath) {
      window.open(apiFileUrl('/api/download', filePath), '_blank');
    }

    function showShareModal(path) {
      document.getElementById('shareFilePath').value = path;
      document.getElementById('sharePassword').value = '';
      document.getElementById('shareExpiry').value = '1d';
      document.getElementById('shareModal').classList.add('active');
    }

    async function createShare(event) {
      event.preventDefault();
      const filePath = document.getElementById('shareFilePath').value;
      const password = document.getElementById('sharePassword').value;
      const expiresIn = document.getElementById('shareExpiry').value;

      showLoading(true);
      closeModal('shareModal');
      try {
        const response = await fetch('/api/share', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ filePath: filePath, password: password, expiresIn: expiresIn })
        });
        const data = await response.json();
        if (data.success) {
          document.getElementById('shareResultUrl').value = window.location.origin + data.shareUrl;
          document.getElementById('shareResultModal').classList.add('active');
        } else {
          showToast('创建分享链接失败: ' + (data.message || '未知错误'), 'error');
        }
      } catch (error) {
        showToast('创建分享链接失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    function copyShareLink() {
      const input = document.getElementById('shareResultUrl');
      input.select();
      const text = input.value;
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).then(function () {
          showToast('链接已复制', 'success');
        }).catch(function () {
          document.execCommand('copy');
          showToast('链接已复制', 'success');
        });
      } else {
        document.execCommand('copy');
        showToast('链接已复制', 'success');
      }
    }

    async function logout() {
      try {
        await fetch('/api/logout', { method: 'POST' });
      } finally {
        window.location.href = '/login.html';
      }
    }

    function closeModal(id) {
      document.getElementById(id).classList.remove('active');
    }

    function showLoading(show) {
      document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
    }

    function showToast(message, type) {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast toast-' + (type || 'info');
      toast.textContent = message;
      container.appendChild(toast);
      window.setTimeout(function () {
        toast.remove();
      }, 3000);
    }

    checkAuth();
    loadFiles();
  </script>
</body>
</html>
`;

const FIXED_ADMIN_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>管理后台 - ws01图床</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
</head>
<body>
  <div class="header">
    <div class="logo">ws01图床 管理后台</div>
    <div class="header-actions">
      <button type="button" class="btn btn-secondary" onclick="window.location.href='/'">返回云盘</button>
      <button type="button" class="btn btn-secondary" onclick="logout()">退出登录</button>
    </div>
  </div>

  <div class="container">
    <div class="tabs">
      <button type="button" class="tab active" onclick="switchTab('stats', event)">统计数据</button>
      <button type="button" class="tab" onclick="switchTab('shares', event)">分享链接</button>
      <button type="button" class="tab" onclick="switchTab('users', event)">授权用户</button>
    </div>

    <div id="statsTab" class="tab-content active">
      <div class="stats-grid">
        <div class="stat-card"><div class="stat-value" id="totalShares">0</div><div class="stat-label">总分享链接数</div></div>
        <div class="stat-card"><div class="stat-value" id="totalViews">0</div><div class="stat-label">总浏览次数</div></div>
        <div class="stat-card"><div class="stat-value" id="totalDownloads">0</div><div class="stat-label">总下载次数</div></div>
      </div>
    </div>

    <div id="sharesTab" class="tab-content">
      <div class="card">
        <div class="card-header"><div class="card-title">分享链接管理</div></div>
        <div class="table-container">
          <table>
            <thead><tr><th>文件名</th><th>分享ID</th><th>密码保护</th><th>浏览次数</th><th>下载次数</th><th>状态</th><th>操作</th></tr></thead>
            <tbody id="sharesTable"></tbody>
          </table>
        </div>
      </div>
    </div>

    <div id="usersTab" class="tab-content">
      <div class="card">
        <div class="card-header">
          <div class="card-title">授权用户管理</div>
          <button type="button" class="btn btn-primary" onclick="showAddUserModal()">添加用户</button>
        </div>
        <div class="table-container">
          <table>
            <thead><tr><th>邮箱</th><th>角色</th><th>创建时间</th><th>操作</th></tr></thead>
            <tbody id="usersTable"></tbody>
          </table>
        </div>
      </div>
    </div>
  </div>

  <div class="modal-overlay" id="addUserModal">
    <div class="modal">
      <div class="modal-header">
        <div class="modal-title">添加授权用户</div>
        <button type="button" class="modal-close" onclick="closeModal('addUserModal')">&times;</button>
      </div>
      <form onsubmit="addUser(event)">
        <div class="form-group">
          <label class="form-label" for="newUserEmail">邮箱</label>
          <input type="email" id="newUserEmail" class="form-input" placeholder="请输入邮箱" required>
        </div>
        <div class="form-group">
          <label class="form-label" for="newUserPassword">密码</label>
          <input type="text" id="newUserPassword" class="form-input" placeholder="请输入密码" required>
        </div>
        <button type="submit" class="btn btn-primary" style="width: 100%;">添加用户</button>
      </form>
    </div>
  </div>

  <div class="toast-container" id="toastContainer"></div>
  <div class="loading-overlay" id="loadingOverlay" style="display: none;"><div class="spinner"></div></div>

  <script>
    async function checkAdminAuth() {
      try {
        const response = await fetch('/api/auth/check');
        const data = await response.json();
        if (!data.authenticated || data.role !== 'admin') {
          window.location.href = '/login.html';
        }
      } catch (error) {
        window.location.href = '/login.html';
      }
    }

    function switchTab(tab, event) {
      document.querySelectorAll('.tab').forEach(function (item) {
        item.classList.remove('active');
      });
      document.querySelectorAll('.tab-content').forEach(function (item) {
        item.classList.remove('active');
      });
      event.target.classList.add('active');
      document.getElementById(tab + 'Tab').classList.add('active');
      if (tab === 'stats') loadStats();
      if (tab === 'shares') loadShares();
      if (tab === 'users') loadUsers();
    }

    async function loadStats() {
      try {
        const response = await fetch('/api/admin/stats');
        const data = await response.json();
        if (data.success) {
          document.getElementById('totalShares').textContent = data.totalShares;
          document.getElementById('totalViews').textContent = data.totalViews;
          document.getElementById('totalDownloads').textContent = data.totalDownloads;
        }
      } catch (error) {
        showToast('加载统计数据失败: ' + error.message, 'error');
      }
    }

    async function loadShares() {
      showLoading(true);
      try {
        const response = await fetch('/api/admin/shares');
        const data = await response.json();
        const tbody = document.getElementById('sharesTable');
        tbody.replaceChildren();
        if (!data.success) throw new Error(data.message || '加载失败');
        if (data.shares.length === 0) {
          appendEmptyRow(tbody, 7, '暂无分享链接');
          return;
        }
        data.shares.forEach(function (share) {
          const tr = document.createElement('tr');
          appendCell(tr, share.fileName);
          appendCell(tr, share.shareId);
          appendCell(tr, share.passwordHash ? '是' : '否');
          appendCell(tr, String(share.viewCount || 0));
          appendCell(tr, String(share.downloadCount || 0));
          appendCell(tr, share.isExpired ? '已过期' : '有效');
          const actions = document.createElement('td');
          actions.appendChild(createSmallButton('复制链接', 'btn-secondary', function () {
            copyShareLink(share.shareId);
          }));
          actions.appendChild(createSmallButton('删除', 'btn-danger', function () {
            deleteShare(share.shareId);
          }));
          tr.appendChild(actions);
          tbody.appendChild(tr);
        });
      } catch (error) {
        showToast('加载分享列表失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    async function loadUsers() {
      showLoading(true);
      try {
        const response = await fetch('/api/admin/users');
        const data = await response.json();
        const tbody = document.getElementById('usersTable');
        tbody.replaceChildren();
        if (!data.success) throw new Error(data.message || '加载失败');
        if (data.users.length === 0) {
          appendEmptyRow(tbody, 4, '暂无授权用户');
          return;
        }
        data.users.forEach(function (user) {
          const tr = document.createElement('tr');
          appendCell(tr, user.email);
          appendCell(tr, user.role === 'admin' ? '管理员' : '普通用户');
          appendCell(tr, user.createdAt ? new Date(user.createdAt).toLocaleString() : '-');
          const actions = document.createElement('td');
          actions.appendChild(createSmallButton('撤销授权', 'btn-danger', function () {
            deleteUser(user.email);
          }));
          tr.appendChild(actions);
          tbody.appendChild(tr);
        });
      } catch (error) {
        showToast('加载用户列表失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    function appendCell(tr, value) {
      const td = document.createElement('td');
      td.textContent = value == null ? '' : value;
      tr.appendChild(td);
    }

    function appendEmptyRow(tbody, colspan, message) {
      const tr = document.createElement('tr');
      const td = document.createElement('td');
      td.colSpan = colspan;
      td.style.textAlign = 'center';
      td.style.color = 'var(--text-muted)';
      td.textContent = message;
      tr.appendChild(td);
      tbody.appendChild(tr);
    }

    function createSmallButton(label, className, handler) {
      const button = document.createElement('button');
      button.type = 'button';
      button.className = 'btn btn-sm ' + className;
      button.textContent = label;
      button.addEventListener('click', handler);
      return button;
    }

    function showAddUserModal() {
      document.getElementById('newUserEmail').value = '';
      document.getElementById('newUserPassword').value = '';
      document.getElementById('addUserModal').classList.add('active');
    }

    async function addUser(event) {
      event.preventDefault();
      const email = document.getElementById('newUserEmail').value.trim();
      const password = document.getElementById('newUserPassword').value;
      showLoading(true);
      closeModal('addUserModal');
      try {
        const response = await fetch('/api/admin/users', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email: email, password: password })
        });
        const data = await response.json();
        if (data.success) {
          showToast('用户添加成功', 'success');
          loadUsers();
        } else {
          showToast('添加失败: ' + (data.message || '未知错误'), 'error');
        }
      } catch (error) {
        showToast('添加失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    async function deleteUser(email) {
      if (!window.confirm('确定要撤销该用户的授权吗?')) return;
      showLoading(true);
      try {
        const response = await fetch('/api/admin/users/' + encodeURIComponent(email), { method: 'DELETE' });
        const data = await response.json();
        if (data.success) {
          showToast('用户已删除', 'success');
          loadUsers();
        } else {
          showToast('删除失败: ' + (data.message || '未知错误'), 'error');
        }
      } catch (error) {
        showToast('删除失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    async function deleteShare(shareId) {
      if (!window.confirm('确定要删除该分享链接吗?')) return;
      showLoading(true);
      try {
        const response = await fetch('/api/admin/shares/' + encodeURIComponent(shareId), { method: 'DELETE' });
        const data = await response.json();
        if (data.success) {
          showToast('分享链接已删除', 'success');
          loadShares();
        } else {
          showToast('删除失败: ' + (data.message || '未知错误'), 'error');
        }
      } catch (error) {
        showToast('删除失败: ' + error.message, 'error');
      } finally {
        showLoading(false);
      }
    }

    function copyFileLink(filePath) {
      const url = window.location.origin + '/api/download' + filePath;
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(url).then(function () {
          showToast('链接已复制', 'success');
        }, function (err) {
          showToast('复制失败: ' + err, 'error');
        });
      } else {
        showToast('您的浏览器不支持剪贴板 API', 'error');
      }
    }

    function copyShareLink(shareId) {
      const url = window.location.origin + '/s/' + shareId;
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(url).then(function () {
          showToast('链接已复制', 'success');
        }).catch(function () {
          showToast('复制失败', 'error');
        });
      } else {
        showToast(url, 'info');
      }
    }

    async function logout() {
      try {
        await fetch('/api/logout', { method: 'POST' });
      } finally {
        window.location.href = '/login.html';
      }
    }

    function closeModal(id) {
      document.getElementById(id).classList.remove('active');
    }

    function showLoading(show) {
      document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
    }

    function showToast(message, type) {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast toast-' + (type || 'info');
      toast.textContent = message;
      container.appendChild(toast);
      window.setTimeout(function () {
        toast.remove();
      }, 3000);
    }

    checkAdminAuth();
    loadStats();
  </script>
</body>
</html>
`;

const FIXED_SHARE_PAGE = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件分享 - ws01图床</title>
<link rel="shortcut icon" href="https://freemjj.rr.kg/api/download/ws01/img/logo/logo04.png">
  ${CSS_STYLES}
</head>
<body>
  <div class="share-container">
    <div class="share-card">
      <div id="loadingState">
        <div class="spinner" style="margin: 0 auto 20px;"></div>
        <div>加载中...</div>
      </div>

      <div id="expiredState" style="display: none;">
        <div class="share-expired">分享链接已过期或不存在</div>
        <p style="color: var(--text-muted); margin-top: 16px;">请联系分享者获取新的链接</p>
      </div>

      <div id="shareContent" style="display: none;">
        <div class="share-icon">📄</div>
        <div class="share-filename" id="fileName"></div>
        <div class="share-filesize" id="fileSize"></div>
        <div id="passwordForm" style="display: none;">
          <div class="form-group">
            <label class="form-label" for="sharePassword">请输入分享密码</label>
            <input type="password" id="sharePassword" class="form-input" placeholder="输入密码">
          </div>
        </div>
        <button type="button" class="btn btn-primary" style="width: 100%; margin-top: 20px;" onclick="downloadFile()">下载文件</button>
      </div>
    </div>
  </div>

  <div class="toast-container" id="toastContainer"></div>

  <script>
    let shareId = '';
    let requiresPassword = false;

    async function loadShareInfo() {
      const parts = window.location.pathname.split('/').filter(Boolean);
      shareId = parts[1] || '';
      if (!shareId) {
        showExpired();
        return;
      }

      try {
        const response = await fetch('/api/share/' + encodeURIComponent(shareId));
        const data = await response.json();
        if (!data.success) {
          showExpired();
          return;
        }
        document.getElementById('loadingState').style.display = 'none';
        document.getElementById('shareContent').style.display = 'block';
        document.getElementById('fileName').textContent = data.fileName;
        document.getElementById('fileSize').textContent = data.fileSizeFormatted;
        requiresPassword = !!data.requiresPassword;
        document.getElementById('passwordForm').style.display = requiresPassword ? 'block' : 'none';
      } catch (error) {
        showExpired();
      }
    }

    function showExpired() {
      document.getElementById('loadingState').style.display = 'none';
      document.getElementById('expiredState').style.display = 'block';
    }

    async function downloadFile() {
      const password = document.getElementById('sharePassword') ? document.getElementById('sharePassword').value : '';
      if (requiresPassword && !password) {
        showToast('请输入分享密码', 'error');
        return;
      }

      try {
        const response = await fetch('/api/share/' + encodeURIComponent(shareId) + '/download', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ password: password })
        });
        if (!response.ok) {
          const data = await response.json();
          showToast(data.message || '下载失败', 'error');
          return;
        }

        const blob = await response.blob();
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = getFilenameFromDisposition(response.headers.get('Content-Disposition'));
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        showToast('下载开始', 'success');
      } catch (error) {
        showToast('下载失败: ' + error.message, 'error');
      }
    }

    function getFilenameFromDisposition(header) {
      if (!header) return 'download';
      const utf8Match = header.match(/filename\\*=UTF-8''([^;\\n]+)/i);
      if (utf8Match) {
        try {
          return decodeURIComponent(utf8Match[1]);
        } catch (error) {
          return utf8Match[1];
        }
      }
      const fallbackMatch = header.match(/filename=["']?([^"';\\n]+)/i);
      return fallbackMatch ? fallbackMatch[1] : 'download';
    }

    function showToast(message, type) {
      const container = document.getElementById('toastContainer');
      const toast = document.createElement('div');
      toast.className = 'toast toast-' + (type || 'info');
      toast.textContent = message;
      container.appendChild(toast);
      window.setTimeout(function () {
        toast.remove();
      }, 3000);
    }

    loadShareInfo();
  </script>
</body>
</html>
`;

// ============================================================================
// MAIN REQUEST HANDLER
// ============================================================================

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const path = url.pathname;
    const method = request.method;
    
    // CORS headers for API requests
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type'
    };
    
    // Handle CORS preflight
    if (method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders });
    }
    
    try {
      // API Routes
      if (path.startsWith('/api/')) {
        // Auth routes
        if (path === '/api/login' && method === 'POST') {
          return await handleLogin(request, env);
        }
        
        if (path === '/api/logout' && method === 'POST') {
          return await handleLogout();
        }
        
        if (path === '/api/auth/check') {
          return await handleCheckAuth(request, env);
        }

        if (path === '/api/cache/refresh' && method === 'POST') {
          return await handleRefreshDirectoryCache(request, env);
        }
        
        // File management routes
        if (path.startsWith('/api/files')) {
          const filePath = safeDecodePath(path.slice('/api/files'.length) || '/');
          
          if (method === 'GET') {
            return await handleListFiles(request, env, filePath);
          }
          if (method === 'POST') {
            return await handleUploadFile(request, env, filePath);
          }
          if (method === 'PUT') {
            return await handleRenameFile(request, env, filePath);
          }
          if (method === 'DELETE') {
            return await handleDeleteFile(request, env, filePath);
          }
        }
        
        // Folder creation
        if (path === '/api/folders' && method === 'POST') {
          return await handleCreateFolder(request, env);
        }
        
        // Download route
        if (path.startsWith('/api/download')) {
          const filePath = safeDecodePath(path.slice('/api/download'.length));
          return await handleDownloadFile(request, env, filePath);
        }
        
        // Preview route
        if (path.startsWith('/api/preview')) {
          const filePath = safeDecodePath(path.slice('/api/preview'.length));
          return await handlePreviewFile(request, env, filePath);
        }
        
        // Share routes
        if (path === '/api/share' && method === 'POST') {
          return await handleCreateShare(request, env);
        }
        
        if (path.match(/^\/api\/share\/[^/]+$/) && method === 'GET') {
          const shareId = path.split('/').pop();
          return await handleGetShareInfo(request, env, shareId);
        }
        
        if (path.match(/^\/api\/share\/[^/]+\/download$/) && method === 'POST') {
          const shareId = path.split('/')[3];
          return await handleShareDownload(request, env, shareId);
        }
        
        // Admin routes
        if (path === '/api/admin/stats' && method === 'GET') {
          return await handleGetStats(request, env);
        }
        
        if (path === '/api/admin/shares' && method === 'GET') {
          return await handleListShares(request, env);
        }
        
        if (path.match(/^\/api\/admin\/shares\/[^/]+$/) && method === 'DELETE') {
          const shareId = path.split('/').pop();
          return await handleDeleteShare(request, env, shareId);
        }
        
        if (path === '/api/admin/users' && method === 'GET') {
          return await handleListUsers(request, env);
        }
        
        if (path === '/api/admin/users' && method === 'POST') {
          return await handleCreateUser(request, env);
        }
        
        if (path.match(/^\/api\/admin\/users\/[^/]+$/) && method === 'DELETE') {
          const email = path.split('/').pop();
          return await handleDeleteUser(request, env, email);
        }
        
        return jsonResponse({ success: false, message: 'API 路径不存在' }, 404);
      }
      
      // Share page route
      if (path.startsWith('/s/')) {
        return htmlResponse(FIXED_SHARE_PAGE);
      }
      
      // Static page routes
      if (path === '/login.html' || path === '/login') {
        return htmlResponse(FIXED_LOGIN_PAGE);
      }
      
      if (path === '/admin.html' || path === '/admin') {
        // Check iadmin
        const auth = await verifyAuth(request, env);
        if (!auth || auth.role !== 'admin') {
          return Response.redirect(url.origin + '/login.html', 302);
        }
        return htmlResponse(FIXED_ADMIN_PAGE);
      }
      
      // Root and index - check auth
      if (path === '/' || path === '/index.html') {
        const auth = await verifyAuth(request, env);
        if (!auth) {
          return Response.redirect(url.origin + '/login.html', 302);
        }
        return htmlResponse(FIXED_INDEX_PAGE);
      }
      
      // Default: redirect to root
      return Response.redirect(url.origin + '/', 302);
      
    } catch (error) {
      console.error('Error:', error);
      return jsonResponse({ success: false, message: '服务器错误: ' + error.message }, 500);
    }
  }
};

留言

暂无留言

0 / 100