<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";

type NavKey = "files" | "transfers" | "shares" | "settings";

type FileItem = {
  name: string;
  displayName?: string;
  path?: string;
  type: "file" | "directory";
  size?: number;
  sizeFormatted?: string;
  modifiedAt?: string;
  isDirectory?: boolean;
};

type BridgeResponse = {
  ok: boolean;
  status: number;
  data: Record<string, any>;
};

const nav = ref<NavKey>("files");
const authenticated = ref(false);
const user = ref<Record<string, any> | null>(null);

const appConfig = reactive({
  baseUrl: localStorage.getItem("desktop_api_base") || "https://cs.workyai.cn",
});

const loginForm = reactive({
  username: "",
  password: "",
  captcha: "",
});

const loginState = reactive({
  loading: false,
  error: "",
  needCaptcha: false,
});

const pathState = reactive({
  currentPath: "/",
  loading: false,
  error: "",
  mode: "directory" as "directory" | "search",
});

const files = ref<FileItem[]>([]);
const selectedFileName = ref("");
const searchKeyword = ref("");

const transferTasks = ref([
  { id: "T-20260218-01", name: "需求说明-v3.pdf", speed: "3.5 MB/s", progress: 83, status: "downloading" },
  { id: "T-20260218-02", name: "发布包-2026-02-17.zip", speed: "1.2 MB/s", progress: 42, status: "uploading" },
  { id: "T-20260218-03", name: "工单导出.xlsx", speed: "-", progress: 100, status: "done" },
]);

const toast = reactive({
  visible: false,
  type: "info",
  message: "",
});
let toastTimer: ReturnType<typeof setTimeout> | null = null;

const navItems = computed(() => [
  { key: "files" as const, label: "全部文件", hint: `${files.value.length} 项` },
  { key: "transfers" as const, label: "传输列表", hint: `${transferTasks.value.length} 个任务` },
  { key: "shares" as const, label: "我的分享", hint: "分享管理" },
  { key: "settings" as const, label: "客户端设置", hint: "连接与偏好" },
]);

const filteredFiles = computed(() => {
  const key = searchKeyword.value.trim().toLowerCase();
  if (!key) return files.value;
  return files.value.filter((item) => {
    const name = String(item.displayName || item.name || "").toLowerCase();
    return name.includes(key);
  });
});

const breadcrumbs = computed(() => {
  const normalized = normalizePath(pathState.currentPath);
  if (normalized === "/") {
    return [{ label: "全部文件", path: "/" }];
  }
  const segments = normalized.split("/").filter(Boolean);
  const nodes = [{ label: "全部文件", path: "/" }];
  let cursor = "";
  for (const seg of segments) {
    cursor += `/${seg}`;
    nodes.push({ label: seg, path: cursor });
  }
  return nodes;
});

const selectedFile = computed(() => {
  if (!selectedFileName.value) return null;
  return files.value.find((item) => item.name === selectedFileName.value) || null;
});

const fileStats = computed(() => {
  const folders = files.value.filter((item) => item.isDirectory || item.type === "directory").length;
  const docs = files.value.length - folders;
  const totalBytes = files.value.reduce((sum, item) => sum + Number(item.size || 0), 0);
  return {
    folders,
    docs,
    total: files.value.length,
    totalBytes,
  };
});

function mapApiItem(raw: Record<string, any>): FileItem {
  const isDirectory = Boolean(raw?.isDirectory || raw?.is_directory || raw?.type === "directory" || raw?.type === "d");
  const fallbackName = String(raw?.name || raw?.displayName || raw?.file_name || "").trim();
  return {
    name: fallbackName,
    displayName: String(raw?.displayName || fallbackName),
    path: typeof raw?.path === "string" ? raw.path : (typeof raw?.file_path === "string" ? raw.file_path : undefined),
    type: isDirectory ? "directory" : "file",
    size: Number(raw?.size || 0),
    sizeFormatted: raw?.sizeFormatted || raw?.size_formatted || undefined,
    modifiedAt: raw?.modifiedAt || raw?.modified_at || raw?.modifyTime || raw?.modifiedTime || raw?.updatedAt || undefined,
    isDirectory,
  };
}

function normalizePath(rawPath: string) {
  const source = String(rawPath || "/").replace(/\\/g, "/");
  const normalized = source.replace(/\/+/g, "/");
  if (!normalized || normalized === ".") return "/";
  return normalized.startsWith("/") ? normalized : `/${normalized}`;
}

function formatDate(value: string | undefined) {
  if (!value) return "-";
  const raw = String(value).trim();
  if (!raw) return "-";
  const localMatch = raw.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
  let date: Date | null = null;
  if (localMatch) {
    date = new Date(
      Number(localMatch[1]),
      Number(localMatch[2]) - 1,
      Number(localMatch[3]),
      Number(localMatch[4] || 0),
      Number(localMatch[5] || 0),
      Number(localMatch[6] || 0),
    );
  } else {
    const fallback = new Date(raw);
    if (!Number.isNaN(fallback.getTime())) {
      date = fallback;
    }
  }
  if (!date) return raw;
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
}

function formatBytes(value: number | undefined) {
  const bytes = Number(value || 0);
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
  const units = ["B", "KB", "MB", "GB", "TB"];
  let num = bytes;
  let unit = 0;
  while (num >= 1024 && unit < units.length - 1) {
    num /= 1024;
    unit += 1;
  }
  const fixed = num >= 10 || unit === 0 ? 1 : 2;
  return `${num.toFixed(fixed)} ${units[unit]}`;
}

function fileTypeLabel(item: FileItem) {
  if (item.isDirectory || item.type === "directory") return "文件夹";
  return "文件";
}

function fileIcon(item: FileItem) {
  if (item.isDirectory || item.type === "directory") return "📁";
  const name = String(item.name || "").toLowerCase();
  if (/\.(jpg|jpeg|png|webp|gif|bmp|svg)$/.test(name)) return "🖼️";
  if (/\.(mp4|mkv|mov|avi)$/.test(name)) return "🎬";
  if (/\.(mp3|wav|flac|aac)$/.test(name)) return "🎵";
  if (/\.(zip|rar|7z|tar|gz)$/.test(name)) return "🗜️";
  if (/\.(pdf)$/.test(name)) return "📕";
  if (/\.(doc|docx)$/.test(name)) return "📘";
  if (/\.(xls|xlsx)$/.test(name)) return "📗";
  return "📄";
}

function showToast(message: string, type = "info") {
  toast.message = message;
  toast.type = type;
  toast.visible = true;
  if (toastTimer) clearTimeout(toastTimer);
  toastTimer = setTimeout(() => {
    toast.visible = false;
  }, 2500);
}

async function invokeBridge(command: string, payload: Record<string, any>) {
  try {
    return await invoke<BridgeResponse>(command, payload);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return {
      ok: false,
      status: 0,
      data: {
        success: false,
        message,
      },
    } satisfies BridgeResponse;
  }
}

async function loadProfile() {
  const response = await invokeBridge("api_get_profile", { baseUrl: appConfig.baseUrl });
  if (response.ok && response.data?.success && response.data?.user) {
    user.value = response.data.user;
    return true;
  }
  return false;
}

async function loadFiles(targetPath = pathState.currentPath) {
  pathState.loading = true;
  pathState.error = "";
  const normalizedPath = normalizePath(targetPath);
  const response = await invokeBridge("api_list_files", {
    baseUrl: appConfig.baseUrl,
    path: normalizedPath,
  });
  if (response.ok && response.data?.success) {
    files.value = Array.isArray(response.data.items) ? response.data.items.map((item: Record<string, any>) => mapApiItem(item)) : [];
    pathState.currentPath = normalizePath(response.data.path || normalizedPath);
    pathState.mode = "directory";
    selectedFileName.value = files.value[0]?.name || "";
  } else {
    pathState.error = response.data?.message || "读取文件列表失败";
    showToast(pathState.error, "error");
  }
  pathState.loading = false;
}

async function restoreSession() {
  const ok = await loadProfile();
  if (!ok) return;
  authenticated.value = true;
  await loadFiles("/");
}

function buildItemPath(item: FileItem) {
  if (item.path && item.path.trim()) {
    return normalizePath(item.path);
  }
  return normalizePath(`${pathState.currentPath}/${item.name}`);
}

function getItemParentPath(item: FileItem) {
  const fullPath = buildItemPath(item);
  const segments = fullPath.split("/").filter(Boolean);
  if (segments.length <= 1) return "/";
  return `/${segments.slice(0, -1).join("/")}`;
}

async function runGlobalSearch() {
  const keyword = searchKeyword.value.trim();
  if (!keyword) {
    await loadFiles(pathState.currentPath);
    return;
  }
  pathState.loading = true;
  pathState.error = "";
  const response = await invokeBridge("api_search_files", {
    baseUrl: appConfig.baseUrl,
    path: pathState.currentPath,
    keyword,
    searchType: "all",
    limit: 200,
  });

  if (response.ok && response.data?.success) {
    files.value = Array.isArray(response.data.items) ? response.data.items.map((item: Record<string, any>) => mapApiItem(item)) : [];
    pathState.mode = "search";
    selectedFileName.value = files.value[0]?.name || "";
    showToast(`搜索完成，共 ${files.value.length} 条结果`, "success");
  } else {
    pathState.error = response.data?.message || "搜索失败";
    showToast(pathState.error, "error");
  }
  pathState.loading = false;
}

async function handleLogin() {
  if (loginState.loading) return;
  loginState.loading = true;
  loginState.error = "";

  const response = await invokeBridge("api_login", {
    baseUrl: appConfig.baseUrl,
    username: loginForm.username.trim(),
    password: loginForm.password,
    captcha: loginForm.captcha.trim() || null,
  });

  loginState.loading = false;
  if (response.ok && response.data?.success) {
    authenticated.value = true;
    user.value = response.data.user || null;
    nav.value = "files";
    showToast("登录成功，正在同步文件目录", "success");
    await loadFiles("/");
    if (!user.value) {
      await loadProfile();
    }
    return;
  }

  loginState.needCaptcha = !!response.data?.needCaptcha;
  loginState.error = response.data?.message || "登录失败";
  showToast(loginState.error, "error");
}

async function handleLogout() {
  await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
  authenticated.value = false;
  user.value = null;
  files.value = [];
  selectedFileName.value = "";
  loginForm.password = "";
  nav.value = "files";
  showToast("已退出客户端", "info");
}

async function applySettings() {
  const nextUrl = appConfig.baseUrl.trim().replace(/\/+$/, "");
  if (!nextUrl.startsWith("http://") && !nextUrl.startsWith("https://")) {
    showToast("API 地址必须以 http:// 或 https:// 开头", "error");
    return;
  }
  appConfig.baseUrl = nextUrl;
  localStorage.setItem("desktop_api_base", nextUrl);
  showToast("已保存服务地址", "success");
  if (authenticated.value) {
    await restoreSession();
  }
}

async function createFolder() {
  if (pathState.loading) return;
  const folderName = window.prompt("请输入新文件夹名称");
  if (!folderName || !folderName.trim()) return;
  const response = await invokeBridge("api_mkdir", {
    baseUrl: appConfig.baseUrl,
    path: pathState.currentPath,
    folderName: folderName.trim(),
  });
  if (response.ok && response.data?.success) {
    showToast("文件夹创建成功", "success");
    await loadFiles(pathState.currentPath);
    return;
  }
  showToast(response.data?.message || "创建文件夹失败", "error");
}

async function renameSelected() {
  const current = selectedFile.value;
  if (!current) {
    showToast("请先选中文件或文件夹", "info");
    return;
  }
  const nextName = window.prompt("请输入新的名称", current.name);
  if (!nextName || !nextName.trim() || nextName.trim() === current.name) return;
  const response = await invokeBridge("api_rename_file", {
    baseUrl: appConfig.baseUrl,
    path: getItemParentPath(current),
    oldName: current.name,
    newName: nextName.trim(),
  });
  if (response.ok && response.data?.success) {
    showToast("重命名成功", "success");
    await loadFiles(pathState.currentPath);
    return;
  }
  showToast(response.data?.message || "重命名失败", "error");
}

async function deleteSelected() {
  const current = selectedFile.value;
  if (!current) {
    showToast("请先选中文件或文件夹", "info");
    return;
  }
  const confirmed = window.confirm(`确认删除「${current.displayName || current.name}」吗？`);
  if (!confirmed) return;

  const response = await invokeBridge("api_delete_file", {
    baseUrl: appConfig.baseUrl,
    path: getItemParentPath(current),
    fileName: current.name,
  });
  if (response.ok && response.data?.success) {
    showToast("删除成功", "success");
    await loadFiles(pathState.currentPath);
    return;
  }
  showToast(response.data?.message || "删除失败", "error");
}

async function downloadSelected() {
  const current = selectedFile.value;
  if (!current) {
    showToast("请先选中文件", "info");
    return;
  }
  if (current.isDirectory || current.type === "directory") {
    showToast("当前仅支持下载文件", "info");
    return;
  }
  const targetPath = buildItemPath(current);
  const response = await invokeBridge("api_get_download_url", {
    baseUrl: appConfig.baseUrl,
    path: targetPath,
    mode: "download",
  });
  if (response.ok && response.data?.success && response.data?.downloadUrl) {
    await openUrl(response.data.downloadUrl);
    showToast("已调用系统浏览器开始下载", "success");
    transferTasks.value = [
      {
        id: `D-${Date.now()}`,
        name: current.displayName || current.name,
        speed: "直连",
        progress: 100,
        status: "dispatched",
      },
      ...transferTasks.value.slice(0, 29),
    ];
    return;
  }
  showToast(response.data?.message || "获取下载链接失败", "error");
}

function selectFile(item: FileItem) {
  selectedFileName.value = item.name;
}

async function openItem(item: FileItem) {
  selectFile(item);
  if (item.isDirectory || item.type === "directory") {
    const nextPath = buildItemPath(item);
    await loadFiles(nextPath);
  }
}

async function jumpToPath(nextPath: string) {
  await loadFiles(nextPath);
}

onMounted(async () => {
  await restoreSession();
});
</script>

<template>
  <div class="desktop-root">
    <div v-if="!authenticated" class="login-shell">
      <section class="login-brand">
        <div class="brand-chip">玩玩云 Desktop</div>
        <h1>企业网盘桌面客户端</h1>
        <p>更快的目录操作、更清晰的传输队列、更像桌面软件的使用体验。</p>
        <div class="brand-grid">
          <div class="brand-card">
            <strong>多任务传输</strong>
            <span>上传下载独立队列管理</span>
          </div>
          <div class="brand-card">
            <strong>目录式检索</strong>
            <span>路径导航 + 快速筛选</span>
          </div>
          <div class="brand-card">
            <strong>独立工作台</strong>
            <span>文件、分享、设置一体化</span>
          </div>
          <div class="brand-card">
            <strong>可接现有后端</strong>
            <span>直接复用当前接口体系</span>
          </div>
        </div>
      </section>

      <section class="login-panel">
        <h2>登录云盘</h2>
        <label>
          服务地址
          <input v-model="appConfig.baseUrl" type="text" placeholder="https://cs.workyai.cn" />
        </label>
        <label>
          用户名
          <input v-model="loginForm.username" type="text" placeholder="请输入账号" />
        </label>
        <label>
          密码
          <input v-model="loginForm.password" type="password" placeholder="请输入密码" @keyup.enter="handleLogin" />
        </label>
        <label v-if="loginState.needCaptcha">
          验证码
          <input v-model="loginForm.captcha" type="text" placeholder="当前服务要求验证码" />
        </label>

        <button class="primary-btn" :disabled="loginState.loading" @click="handleLogin">
          {{ loginState.loading ? "登录中..." : "进入客户端" }}
        </button>
        <p v-if="loginState.error" class="error-text">{{ loginState.error }}</p>
      </section>
    </div>

    <div v-else class="work-shell">
      <aside class="left-nav">
        <div class="left-header">
          <div class="app-mark">W</div>
          <div>
            <strong>玩玩云</strong>
            <span>Desktop</span>
          </div>
        </div>

        <button
          v-for="item in navItems"
          :key="item.key"
          class="nav-btn"
          :class="{ active: nav === item.key }"
          @click="nav = item.key"
        >
          <span>{{ item.label }}</span>
          <small>{{ item.hint }}</small>
        </button>

        <div class="user-card">
          <div class="avatar">{{ String(user?.username || "U").slice(0, 1).toUpperCase() }}</div>
          <div class="meta">
            <strong>{{ user?.username || "-" }}</strong>
            <span>{{ user?.current_storage_type === "local" ? "本地存储" : "OSS 存储" }}</span>
          </div>
          <button class="ghost-btn" @click="handleLogout">退出</button>
        </div>
      </aside>

      <section class="work-area">
        <header class="top-tools">
          <div class="crumbs">
            <button
              v-for="(crumb, idx) in breadcrumbs"
              :key="crumb.path"
              class="crumb-btn"
              @click="jumpToPath(crumb.path)"
            >
              {{ crumb.label }}<span v-if="idx < breadcrumbs.length - 1"> / </span>
            </button>
          </div>
          <div class="tool-right">
            <input
              v-model="searchKeyword"
              type="text"
              class="search-input"
              placeholder="输入关键词回车全局搜索..."
              @keyup.enter="runGlobalSearch"
            />
            <button class="action-btn" @click="runGlobalSearch">搜索</button>
            <button class="action-btn" @click="createFolder">新建文件夹</button>
            <button class="action-btn" @click="renameSelected">重命名</button>
            <button class="action-btn danger" @click="deleteSelected">删除</button>
            <button class="solid-btn" @click="downloadSelected">下载</button>
            <button class="action-btn" @click="loadFiles(pathState.currentPath)">刷新</button>
          </div>
        </header>

        <main class="main-grid">
          <section class="panel content-panel">
            <template v-if="nav === 'files'">
              <div class="panel-head">
                <h3>文件视图</h3>
                <span>{{ pathState.mode === "search" ? "搜索结果" : "当前目录" }} {{ pathState.currentPath }}</span>
              </div>

              <div v-if="pathState.loading" class="empty-tip">正在加载目录...</div>
              <div v-else-if="pathState.error" class="empty-tip error">{{ pathState.error }}</div>
              <div v-else-if="filteredFiles.length === 0" class="empty-tip">当前目录暂无文件</div>
              <div v-else class="table-wrap">
                <table>
                  <thead>
                    <tr>
                      <th>名称</th>
                      <th>类型</th>
                      <th>大小</th>
                      <th>修改时间</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr
                      v-for="item in filteredFiles"
                      :key="item.name"
                      :class="{ selected: selectedFileName === item.name }"
                      @click="selectFile(item)"
                      @dblclick="openItem(item)"
                    >
                      <td class="name-cell"><span class="icon">{{ fileIcon(item) }}</span>{{ item.displayName || item.name }}</td>
                      <td>{{ fileTypeLabel(item) }}</td>
                      <td>{{ item.isDirectory ? "-" : (item.sizeFormatted || formatBytes(item.size)) }}</td>
                      <td>{{ formatDate(item.modifiedAt) }}</td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </template>

            <template v-else-if="nav === 'transfers'">
              <div class="panel-head">
                <h3>传输任务</h3>
                <span>上传/下载队列</span>
              </div>
              <div class="task-list">
                <div v-for="task in transferTasks" :key="task.id" class="task-row">
                  <div>
                    <strong>{{ task.name }}</strong>
                    <small>{{ task.id }} · {{ task.status }}</small>
                  </div>
                  <div class="task-right">
                    <span>{{ task.speed }}</span>
                    <div class="progress">
                      <div class="bar" :style="{ width: `${task.progress}%` }" />
                    </div>
                  </div>
                </div>
              </div>
            </template>

            <template v-else-if="nav === 'shares'">
              <div class="panel-head">
                <h3>分享管理</h3>
                <span>桌面端下阶段接入分享创建与回收</span>
              </div>
              <div class="placeholder-card">
                <p>当前版本已预留分享模块位，下一步会接入分享链接列表和直链复制。</p>
              </div>
            </template>

            <template v-else>
              <div class="panel-head">
                <h3>客户端设置</h3>
                <span>连接地址与基础偏好</span>
              </div>
              <div class="settings-form">
                <label>
                  API 服务地址
                  <input v-model="appConfig.baseUrl" type="text" placeholder="https://cs.workyai.cn" />
                </label>
                <button class="solid-btn" @click="applySettings">保存设置</button>
              </div>
            </template>
          </section>

          <aside class="panel detail-panel">
            <h3>详情面板</h3>
            <div class="stat-grid">
              <div>
                <strong>{{ fileStats.total }}</strong>
                <span>当前项目数</span>
              </div>
              <div>
                <strong>{{ fileStats.folders }}</strong>
                <span>文件夹</span>
              </div>
              <div>
                <strong>{{ fileStats.docs }}</strong>
                <span>文件数</span>
              </div>
              <div>
                <strong>{{ formatBytes(fileStats.totalBytes) }}</strong>
                <span>目录容量</span>
              </div>
            </div>

            <div v-if="selectedFile" class="selected-info">
              <h4>{{ selectedFile.displayName || selectedFile.name }}</h4>
              <p>类型：{{ fileTypeLabel(selectedFile) }}</p>
              <p>路径：{{ buildItemPath(selectedFile) }}</p>
              <p>大小：{{ selectedFile.isDirectory ? "-" : (selectedFile.sizeFormatted || formatBytes(selectedFile.size)) }}</p>
              <p>时间：{{ formatDate(selectedFile.modifiedAt) }}</p>
            </div>
            <div v-else class="empty-tip">选中一个文件可查看详细信息</div>
          </aside>
        </main>
      </section>
    </div>

    <div v-if="toast.visible" class="toast" :class="toast.type">{{ toast.message }}</div>
  </div>
</template>

<style scoped>
:global(html, body, #app) {
  width: 100%;
  height: 100%;
  margin: 0;
  font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  background: linear-gradient(155deg, #f4f7fb 0%, #eef3ff 40%, #e8eef8 100%);
  color: #1f2b3a;
}

:global(*) {
  box-sizing: border-box;
}

.desktop-root {
  width: 100%;
  height: 100%;
  padding: 18px;
}

.login-shell {
  height: 100%;
  display: grid;
  grid-template-columns: 1.2fr 0.9fr;
  gap: 16px;
}

.login-brand,
.login-panel,
.panel {
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(141, 164, 196, 0.25);
  border-radius: 20px;
  box-shadow: 0 14px 28px rgba(28, 59, 102, 0.08);
}

.login-brand {
  padding: 32px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.brand-chip {
  width: fit-content;
  background: #1d6fff;
  color: #fff;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  letter-spacing: 0.6px;
}

.login-brand h1 {
  margin: 0;
  font-size: 34px;
  line-height: 1.2;
}

.login-brand p {
  margin: 0;
  color: #51667f;
  font-size: 15px;
}

.brand-grid {
  margin-top: 10px;
  display: grid;
  gap: 10px;
  grid-template-columns: 1fr 1fr;
}

.brand-card {
  padding: 16px;
  border-radius: 14px;
  background: #f2f7ff;
  border: 1px solid #d7e5ff;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.brand-card strong {
  font-size: 14px;
}

.brand-card span {
  font-size: 12px;
  color: #5c7189;
}

.login-panel {
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.login-panel h2 {
  margin: 0 0 8px;
  font-size: 23px;
}

label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 13px;
  color: #435872;
}

input {
  width: 100%;
  height: 42px;
  border: 1px solid #d6dfe9;
  border-radius: 12px;
  padding: 0 12px;
  font-size: 14px;
  color: #1f2b3a;
  background: #fff;
}

input:focus {
  border-color: #1d6fff;
  outline: none;
  box-shadow: 0 0 0 3px rgba(29, 111, 255, 0.12);
}

.primary-btn,
.solid-btn,
.ghost-btn {
  height: 40px;
  border-radius: 12px;
  border: 0;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
}

.primary-btn,
.solid-btn {
  background: linear-gradient(90deg, #1d6fff 0%, #3f8cff 100%);
  color: #fff;
}

.primary-btn:disabled {
  opacity: 0.65;
  cursor: not-allowed;
}

.error-text {
  margin: 0;
  color: #d04848;
  font-size: 13px;
}

.work-shell {
  height: 100%;
  display: grid;
  grid-template-columns: 248px 1fr;
  gap: 14px;
}

.left-nav {
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid rgba(141, 164, 196, 0.3);
  border-radius: 20px;
  padding: 16px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.left-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px;
  margin-bottom: 4px;
}

.app-mark {
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: linear-gradient(140deg, #1d6fff, #57a2ff);
  color: #fff;
  display: grid;
  place-items: center;
  font-weight: 800;
}

.left-header strong {
  display: block;
  font-size: 16px;
}

.left-header span {
  font-size: 12px;
  color: #6b8097;
}

.nav-btn {
  text-align: left;
  padding: 10px 12px;
  border-radius: 12px;
  border: 0;
  background: transparent;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-btn span {
  font-size: 14px;
  color: #223244;
}

.nav-btn small {
  color: #6b8097;
  font-size: 11px;
}

.nav-btn:hover {
  background: #eef4ff;
}

.nav-btn.active {
  background: #1d6fff;
}

.nav-btn.active span,
.nav-btn.active small {
  color: #fff;
}

.user-card {
  margin-top: auto;
  background: #eff4fb;
  border-radius: 14px;
  padding: 10px;
  display: grid;
  grid-template-columns: 38px 1fr auto;
  gap: 10px;
  align-items: center;
}

.avatar {
  width: 38px;
  height: 38px;
  border-radius: 12px;
  background: #d9e8ff;
  display: grid;
  place-items: center;
  font-weight: 700;
  color: #2456a8;
}

.user-card .meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.user-card .meta strong {
  font-size: 13px;
}

.user-card .meta span {
  font-size: 11px;
  color: #60768f;
}

.ghost-btn {
  width: 56px;
  height: 28px;
  background: #fff;
  border: 1px solid #d8e2ee;
  font-size: 12px;
}

.work-area {
  display: grid;
  grid-template-rows: 52px 1fr;
  gap: 12px;
}

.top-tools {
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid rgba(141, 164, 196, 0.3);
  border-radius: 16px;
  padding: 0 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

.crumbs {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 2px;
  overflow-x: auto;
}

.crumb-btn {
  border: 0;
  background: transparent;
  color: #3f5876;
  cursor: pointer;
  font-size: 13px;
  white-space: nowrap;
}

.crumb-btn:hover {
  color: #1d6fff;
}

.tool-right {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.search-input {
  width: 260px;
  height: 34px;
  border-radius: 10px;
}

.solid-btn {
  height: 34px;
  padding: 0 14px;
}

.action-btn {
  height: 34px;
  border: 1px solid #cad8ea;
  border-radius: 10px;
  background: #fff;
  color: #284666;
  padding: 0 12px;
  cursor: pointer;
  font-size: 13px;
}

.action-btn:hover {
  background: #f2f7ff;
}

.action-btn.danger {
  border-color: #efc3c3;
  color: #b53f3f;
  background: #fff8f8;
}

.main-grid {
  display: grid;
  grid-template-columns: 1fr 270px;
  gap: 12px;
  min-height: 0;
}

.content-panel,
.detail-panel {
  padding: 14px;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.panel-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.panel-head h3 {
  margin: 0;
  font-size: 16px;
}

.panel-head span {
  color: #60768f;
  font-size: 12px;
}

.table-wrap {
  border: 1px solid #d8e1ee;
  border-radius: 12px;
  overflow: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

thead th {
  position: sticky;
  top: 0;
  background: #f4f8ff;
  color: #4b637f;
  font-size: 12px;
  text-align: left;
  padding: 10px;
  border-bottom: 1px solid #d8e1ee;
}

tbody td {
  border-bottom: 1px solid #edf2f7;
  padding: 10px;
  font-size: 13px;
}

tbody tr {
  cursor: pointer;
}

tbody tr:hover {
  background: #f7faff;
}

tbody tr.selected {
  background: #eaf2ff;
}

.name-cell {
  display: flex;
  align-items: center;
  gap: 8px;
}

.icon {
  width: 22px;
  text-align: center;
}

.task-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.task-row {
  padding: 10px;
  border: 1px solid #d8e1ee;
  border-radius: 12px;
  display: grid;
  grid-template-columns: 1fr 190px;
  gap: 10px;
  align-items: center;
}

.task-row strong {
  display: block;
  font-size: 13px;
}

.task-row small {
  color: #647b94;
}

.task-right {
  display: grid;
  gap: 6px;
}

.task-right span {
  font-size: 12px;
  color: #4a6381;
}

.progress {
  height: 8px;
  background: #e4edf9;
  border-radius: 999px;
  overflow: hidden;
}

.bar {
  height: 100%;
  background: linear-gradient(90deg, #1d6fff, #57a2ff);
}

.placeholder-card {
  border: 1px dashed #b6c8df;
  background: #f5f9ff;
  border-radius: 12px;
  padding: 14px;
}

.placeholder-card p {
  margin: 0;
  color: #4f6984;
  line-height: 1.6;
  font-size: 13px;
}

.settings-form {
  max-width: 520px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.detail-panel h3 {
  margin: 0;
  font-size: 16px;
}

.stat-grid {
  margin-top: 10px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.stat-grid > div {
  border-radius: 10px;
  border: 1px solid #d8e1ee;
  background: #f7faff;
  padding: 10px;
}

.stat-grid strong {
  display: block;
  font-size: 15px;
}

.stat-grid span {
  font-size: 11px;
  color: #60768f;
}

.selected-info {
  margin-top: 12px;
  border-top: 1px solid #dbe6f2;
  padding-top: 12px;
}

.selected-info h4 {
  margin: 0 0 8px;
  font-size: 14px;
}

.selected-info p {
  margin: 0 0 6px;
  font-size: 12px;
  color: #4f6984;
}

.empty-tip {
  margin: auto;
  color: #5a7089;
  font-size: 13px;
}

.empty-tip.error {
  color: #cc4242;
}

.toast {
  position: fixed;
  right: 24px;
  bottom: 24px;
  padding: 10px 14px;
  border-radius: 10px;
  color: #fff;
  font-size: 13px;
  box-shadow: 0 10px 18px rgba(22, 44, 73, 0.18);
}

.toast.info {
  background: #4f6e8f;
}

.toast.success {
  background: #1f9a63;
}

.toast.error {
  background: #d04848;
}

@media (max-width: 1260px) {
  .main-grid {
    grid-template-columns: 1fr;
  }
}
</style>
