From 362237241ef4b2269fc7a1f71a936c7c7c12012e Mon Sep 17 00:00:00 2001 From: engigu Date: Sat, 20 Dec 2025 09:30:16 +0800 Subject: [PATCH 001/892] feat: Initial commit --- .github/workflows/docker.yml | 69 + .gitignore | 36 + Dockerfile | 74 + Dockerfile.debian | 21 + LICENSE | 21 + Makefile | 77 + README.md | 138 + configs/config.json | 24 + docker-compose.yml | 15 + go.mod | 64 + go.sum | 151 + internal/bootstrap/bootstrap.go | 76 + internal/constant/constant.go | 48 + internal/constant/version.go | 12 + internal/controllers/auth_controller.go | 81 + internal/controllers/dashboard_controller.go | 51 + internal/controllers/env_controller.go | 103 + internal/controllers/executor_controller.go | 55 + internal/controllers/file_controller.go | 344 ++ internal/controllers/log_controller.go | 107 + internal/controllers/script_controller.go | 99 + internal/controllers/settings_controller.go | 149 + internal/controllers/task_controller.go | 128 + internal/controllers/terminal_controller.go | 249 ++ internal/database/database.go | 74 + internal/database/migrate.go | 16 + internal/logger/logger.go | 83 + internal/middleware/auth.go | 43 + internal/middleware/logger.go | 54 + internal/models/env.go | 38 + internal/models/setting.go | 17 + internal/models/task.go | 42 + internal/models/time.go | 70 + internal/models/user.go | 23 + internal/router/register.go | 48 + internal/router/router.go | 176 ++ internal/services/config_service.go | 71 + internal/services/cron_service.go | 150 + internal/services/env_service.go | 68 + internal/services/executor_service.go | 183 ++ internal/services/init_service.go | 57 + internal/services/script_service.go | 52 + internal/services/settings_service.go | 69 + internal/services/task_service.go | 71 + internal/services/user_service.go | 67 + internal/static/embed.go | 33 + internal/utils/archive.go | 125 + internal/utils/compress.go | 39 + internal/utils/pagination.go | 45 + internal/utils/response.go | 55 + internal/utils/shell.go | 54 + internal/utils/token.go | 48 + main.go | 7 + web/.gitignore | 24 + web/README.md | 5 + web/components.json | 21 + web/index.html | 13 + web/package-lock.json | 2790 +++++++++++++++++ web/package.json | 44 + web/public/logo.svg | 1 + web/src/App.vue | 9 + web/src/api/index.ts | 254 ++ web/src/assets/index.css | 145 + web/src/assets/vue.svg | 1 + web/src/components/FileTreeNode.vue | 122 + web/src/components/Pagination.vue | 44 + web/src/components/ThemeToggle.vue | 39 + .../ui/alert-dialog/AlertDialog.vue | 15 + .../ui/alert-dialog/AlertDialogAction.vue | 18 + .../ui/alert-dialog/AlertDialogCancel.vue | 25 + .../ui/alert-dialog/AlertDialogContent.vue | 44 + .../alert-dialog/AlertDialogDescription.vue | 23 + .../ui/alert-dialog/AlertDialogFooter.vue | 22 + .../ui/alert-dialog/AlertDialogHeader.vue | 17 + .../ui/alert-dialog/AlertDialogTitle.vue | 21 + .../ui/alert-dialog/AlertDialogTrigger.vue | 12 + web/src/components/ui/alert-dialog/index.ts | 9 + web/src/components/ui/badge/Badge.vue | 26 + web/src/components/ui/badge/index.ts | 26 + web/src/components/ui/button/Button.vue | 29 + web/src/components/ui/button/index.ts | 38 + web/src/components/ui/card/Card.vue | 22 + web/src/components/ui/card/CardAction.vue | 17 + web/src/components/ui/card/CardContent.vue | 17 + .../components/ui/card/CardDescription.vue | 17 + web/src/components/ui/card/CardFooter.vue | 17 + web/src/components/ui/card/CardHeader.vue | 17 + web/src/components/ui/card/CardTitle.vue | 17 + web/src/components/ui/card/index.ts | 7 + web/src/components/ui/dialog/Dialog.vue | 19 + web/src/components/ui/dialog/DialogClose.vue | 15 + .../components/ui/dialog/DialogContent.vue | 53 + .../ui/dialog/DialogDescription.vue | 23 + web/src/components/ui/dialog/DialogFooter.vue | 15 + web/src/components/ui/dialog/DialogHeader.vue | 17 + .../components/ui/dialog/DialogOverlay.vue | 21 + .../ui/dialog/DialogScrollContent.vue | 59 + web/src/components/ui/dialog/DialogTitle.vue | 23 + .../components/ui/dialog/DialogTrigger.vue | 15 + web/src/components/ui/dialog/index.ts | 10 + web/src/components/ui/drawer/Drawer.vue | 23 + web/src/components/ui/drawer/DrawerClose.vue | 15 + .../components/ui/drawer/DrawerContent.vue | 38 + .../ui/drawer/DrawerDescription.vue | 21 + web/src/components/ui/drawer/DrawerFooter.vue | 17 + web/src/components/ui/drawer/DrawerHeader.vue | 17 + .../components/ui/drawer/DrawerOverlay.vue | 19 + web/src/components/ui/drawer/DrawerTitle.vue | 21 + .../components/ui/drawer/DrawerTrigger.vue | 15 + web/src/components/ui/drawer/index.ts | 9 + .../ui/dropdown-menu/DropdownMenu.vue | 19 + .../DropdownMenuCheckboxItem.vue | 39 + .../ui/dropdown-menu/DropdownMenuContent.vue | 39 + .../ui/dropdown-menu/DropdownMenuGroup.vue | 15 + .../ui/dropdown-menu/DropdownMenuItem.vue | 31 + .../ui/dropdown-menu/DropdownMenuLabel.vue | 23 + .../dropdown-menu/DropdownMenuRadioGroup.vue | 21 + .../dropdown-menu/DropdownMenuRadioItem.vue | 40 + .../dropdown-menu/DropdownMenuSeparator.vue | 23 + .../ui/dropdown-menu/DropdownMenuShortcut.vue | 17 + .../ui/dropdown-menu/DropdownMenuSub.vue | 18 + .../dropdown-menu/DropdownMenuSubContent.vue | 27 + .../dropdown-menu/DropdownMenuSubTrigger.vue | 30 + .../ui/dropdown-menu/DropdownMenuTrigger.vue | 17 + web/src/components/ui/dropdown-menu/index.ts | 16 + web/src/components/ui/input/Input.vue | 33 + web/src/components/ui/input/index.ts | 1 + web/src/components/ui/label/Label.vue | 26 + web/src/components/ui/label/index.ts | 1 + .../components/ui/radio-group/RadioGroup.vue | 25 + .../ui/radio-group/RadioGroupItem.vue | 40 + web/src/components/ui/radio-group/index.ts | 2 + .../components/ui/scroll-area/ScrollArea.vue | 33 + .../components/ui/scroll-area/ScrollBar.vue | 32 + web/src/components/ui/scroll-area/index.ts | 2 + web/src/components/ui/separator/Separator.vue | 29 + web/src/components/ui/separator/index.ts | 1 + web/src/components/ui/sonner/Sonner.vue | 16 + web/src/components/ui/sonner/index.ts | 1 + web/src/components/ui/switch/Switch.vue | 38 + web/src/components/ui/switch/index.ts | 1 + web/src/components/ui/table/Table.vue | 16 + web/src/components/ui/table/TableBody.vue | 17 + web/src/components/ui/table/TableCaption.vue | 17 + web/src/components/ui/table/TableCell.vue | 22 + web/src/components/ui/table/TableEmpty.vue | 34 + web/src/components/ui/table/TableFooter.vue | 17 + web/src/components/ui/table/TableHead.vue | 17 + web/src/components/ui/table/TableHeader.vue | 17 + web/src/components/ui/table/TableRow.vue | 17 + web/src/components/ui/table/index.ts | 9 + web/src/components/ui/table/utils.ts | 10 + web/src/components/ui/tabs/Tabs.vue | 24 + web/src/components/ui/tabs/TabsContent.vue | 21 + web/src/components/ui/tabs/TabsList.vue | 24 + web/src/components/ui/tabs/TabsTrigger.vue | 26 + web/src/components/ui/tabs/index.ts | 4 + web/src/components/ui/textarea/Textarea.vue | 28 + web/src/components/ui/textarea/index.ts | 1 + web/src/components/ui/tooltip/Tooltip.vue | 19 + .../components/ui/tooltip/TooltipContent.vue | 34 + .../components/ui/tooltip/TooltipProvider.vue | 14 + .../components/ui/tooltip/TooltipTrigger.vue | 15 + web/src/components/ui/tooltip/index.ts | 4 + web/src/composables/useTheme.ts | 49 + web/src/layouts/MainLayout.vue | 97 + web/src/lib/utils.ts | 7 + web/src/main.ts | 15 + web/src/router/index.ts | 53 + web/src/types/jinrishici.d.ts | 18 + web/src/utils/decompress.ts | 19 + web/src/views/dashboard/Dashboard.vue | 84 + web/src/views/editor/Editor.vue | 424 +++ web/src/views/environments/Environments.vue | 200 ++ web/src/views/history/History.vue | 216 ++ web/src/views/history/LogViewer.vue | 65 + web/src/views/login/Login.vue | 68 + web/src/views/scripts/Scripts.vue | 376 +++ web/src/views/settings/AboutSettings.vue | 80 + web/src/views/settings/LogsSettings.vue | 38 + web/src/views/settings/PasswordSettings.vue | 54 + web/src/views/settings/Settings.vue | 73 + web/src/views/settings/SiteSettings.vue | 33 + web/src/views/tasks/Tasks.vue | 213 ++ web/src/views/terminal/Terminal.vue | 208 ++ web/tsconfig.app.json | 18 + web/tsconfig.json | 13 + web/tsconfig.node.json | 26 + web/vite.config.ts | 22 + 189 files changed, 12035 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Dockerfile.debian create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 configs/config.json create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/bootstrap/bootstrap.go create mode 100644 internal/constant/constant.go create mode 100644 internal/constant/version.go create mode 100644 internal/controllers/auth_controller.go create mode 100644 internal/controllers/dashboard_controller.go create mode 100644 internal/controllers/env_controller.go create mode 100644 internal/controllers/executor_controller.go create mode 100644 internal/controllers/file_controller.go create mode 100644 internal/controllers/log_controller.go create mode 100644 internal/controllers/script_controller.go create mode 100644 internal/controllers/settings_controller.go create mode 100644 internal/controllers/task_controller.go create mode 100644 internal/controllers/terminal_controller.go create mode 100644 internal/database/database.go create mode 100644 internal/database/migrate.go create mode 100644 internal/logger/logger.go create mode 100644 internal/middleware/auth.go create mode 100644 internal/middleware/logger.go create mode 100644 internal/models/env.go create mode 100644 internal/models/setting.go create mode 100644 internal/models/task.go create mode 100644 internal/models/time.go create mode 100644 internal/models/user.go create mode 100644 internal/router/register.go create mode 100644 internal/router/router.go create mode 100644 internal/services/config_service.go create mode 100644 internal/services/cron_service.go create mode 100644 internal/services/env_service.go create mode 100644 internal/services/executor_service.go create mode 100644 internal/services/init_service.go create mode 100644 internal/services/script_service.go create mode 100644 internal/services/settings_service.go create mode 100644 internal/services/task_service.go create mode 100644 internal/services/user_service.go create mode 100644 internal/static/embed.go create mode 100644 internal/utils/archive.go create mode 100644 internal/utils/compress.go create mode 100644 internal/utils/pagination.go create mode 100644 internal/utils/response.go create mode 100644 internal/utils/shell.go create mode 100644 internal/utils/token.go create mode 100644 main.go create mode 100644 web/.gitignore create mode 100644 web/README.md create mode 100644 web/components.json create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/logo.svg create mode 100644 web/src/App.vue create mode 100644 web/src/api/index.ts create mode 100644 web/src/assets/index.css create mode 100644 web/src/assets/vue.svg create mode 100644 web/src/components/FileTreeNode.vue create mode 100644 web/src/components/Pagination.vue create mode 100644 web/src/components/ThemeToggle.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialog.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogAction.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogCancel.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogContent.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogDescription.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogFooter.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogHeader.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogTitle.vue create mode 100644 web/src/components/ui/alert-dialog/AlertDialogTrigger.vue create mode 100644 web/src/components/ui/alert-dialog/index.ts create mode 100644 web/src/components/ui/badge/Badge.vue create mode 100644 web/src/components/ui/badge/index.ts create mode 100644 web/src/components/ui/button/Button.vue create mode 100644 web/src/components/ui/button/index.ts create mode 100644 web/src/components/ui/card/Card.vue create mode 100644 web/src/components/ui/card/CardAction.vue create mode 100644 web/src/components/ui/card/CardContent.vue create mode 100644 web/src/components/ui/card/CardDescription.vue create mode 100644 web/src/components/ui/card/CardFooter.vue create mode 100644 web/src/components/ui/card/CardHeader.vue create mode 100644 web/src/components/ui/card/CardTitle.vue create mode 100644 web/src/components/ui/card/index.ts create mode 100644 web/src/components/ui/dialog/Dialog.vue create mode 100644 web/src/components/ui/dialog/DialogClose.vue create mode 100644 web/src/components/ui/dialog/DialogContent.vue create mode 100644 web/src/components/ui/dialog/DialogDescription.vue create mode 100644 web/src/components/ui/dialog/DialogFooter.vue create mode 100644 web/src/components/ui/dialog/DialogHeader.vue create mode 100644 web/src/components/ui/dialog/DialogOverlay.vue create mode 100644 web/src/components/ui/dialog/DialogScrollContent.vue create mode 100644 web/src/components/ui/dialog/DialogTitle.vue create mode 100644 web/src/components/ui/dialog/DialogTrigger.vue create mode 100644 web/src/components/ui/dialog/index.ts create mode 100644 web/src/components/ui/drawer/Drawer.vue create mode 100644 web/src/components/ui/drawer/DrawerClose.vue create mode 100644 web/src/components/ui/drawer/DrawerContent.vue create mode 100644 web/src/components/ui/drawer/DrawerDescription.vue create mode 100644 web/src/components/ui/drawer/DrawerFooter.vue create mode 100644 web/src/components/ui/drawer/DrawerHeader.vue create mode 100644 web/src/components/ui/drawer/DrawerOverlay.vue create mode 100644 web/src/components/ui/drawer/DrawerTitle.vue create mode 100644 web/src/components/ui/drawer/DrawerTrigger.vue create mode 100644 web/src/components/ui/drawer/index.ts create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenu.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuContent.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuItem.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuSub.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue create mode 100644 web/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue create mode 100644 web/src/components/ui/dropdown-menu/index.ts create mode 100644 web/src/components/ui/input/Input.vue create mode 100644 web/src/components/ui/input/index.ts create mode 100644 web/src/components/ui/label/Label.vue create mode 100644 web/src/components/ui/label/index.ts create mode 100644 web/src/components/ui/radio-group/RadioGroup.vue create mode 100644 web/src/components/ui/radio-group/RadioGroupItem.vue create mode 100644 web/src/components/ui/radio-group/index.ts create mode 100644 web/src/components/ui/scroll-area/ScrollArea.vue create mode 100644 web/src/components/ui/scroll-area/ScrollBar.vue create mode 100644 web/src/components/ui/scroll-area/index.ts create mode 100644 web/src/components/ui/separator/Separator.vue create mode 100644 web/src/components/ui/separator/index.ts create mode 100644 web/src/components/ui/sonner/Sonner.vue create mode 100644 web/src/components/ui/sonner/index.ts create mode 100644 web/src/components/ui/switch/Switch.vue create mode 100644 web/src/components/ui/switch/index.ts create mode 100644 web/src/components/ui/table/Table.vue create mode 100644 web/src/components/ui/table/TableBody.vue create mode 100644 web/src/components/ui/table/TableCaption.vue create mode 100644 web/src/components/ui/table/TableCell.vue create mode 100644 web/src/components/ui/table/TableEmpty.vue create mode 100644 web/src/components/ui/table/TableFooter.vue create mode 100644 web/src/components/ui/table/TableHead.vue create mode 100644 web/src/components/ui/table/TableHeader.vue create mode 100644 web/src/components/ui/table/TableRow.vue create mode 100644 web/src/components/ui/table/index.ts create mode 100644 web/src/components/ui/table/utils.ts create mode 100644 web/src/components/ui/tabs/Tabs.vue create mode 100644 web/src/components/ui/tabs/TabsContent.vue create mode 100644 web/src/components/ui/tabs/TabsList.vue create mode 100644 web/src/components/ui/tabs/TabsTrigger.vue create mode 100644 web/src/components/ui/tabs/index.ts create mode 100644 web/src/components/ui/textarea/Textarea.vue create mode 100644 web/src/components/ui/textarea/index.ts create mode 100644 web/src/components/ui/tooltip/Tooltip.vue create mode 100644 web/src/components/ui/tooltip/TooltipContent.vue create mode 100644 web/src/components/ui/tooltip/TooltipProvider.vue create mode 100644 web/src/components/ui/tooltip/TooltipTrigger.vue create mode 100644 web/src/components/ui/tooltip/index.ts create mode 100644 web/src/composables/useTheme.ts create mode 100644 web/src/layouts/MainLayout.vue create mode 100644 web/src/lib/utils.ts create mode 100644 web/src/main.ts create mode 100644 web/src/router/index.ts create mode 100644 web/src/types/jinrishici.d.ts create mode 100644 web/src/utils/decompress.ts create mode 100644 web/src/views/dashboard/Dashboard.vue create mode 100644 web/src/views/editor/Editor.vue create mode 100644 web/src/views/environments/Environments.vue create mode 100644 web/src/views/history/History.vue create mode 100644 web/src/views/history/LogViewer.vue create mode 100644 web/src/views/login/Login.vue create mode 100644 web/src/views/scripts/Scripts.vue create mode 100644 web/src/views/settings/AboutSettings.vue create mode 100644 web/src/views/settings/LogsSettings.vue create mode 100644 web/src/views/settings/PasswordSettings.vue create mode 100644 web/src/views/settings/Settings.vue create mode 100644 web/src/views/settings/SiteSettings.vue create mode 100644 web/src/views/tasks/Tasks.vue create mode 100644 web/src/views/terminal/Terminal.vue create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..237116f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,69 @@ +name: Build and Push Docker Image + +on: + # push: + # branches: [ main ] + # tags: [ 'v*' ] + # pull_request: + # branches: [ main ] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: baihu + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + + - name: Get build time + id: build_time + run: echo "time=$(date '+%Y/%m/%d %H:%M:%S')" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ github.ref_name }} + BUILD_TIME=${{ steps.build_time.outputs.time }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d47f36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Binaries +baihu +*.exe +*.dll +*.so +*.dylib + +# Go +# go.sum should be committed for reproducible builds + +# Data & Logs +data/ +# logs/ +# scripts/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Node +node_modules/ +web/dist/ + +# Embedded static files (built during CI/Docker) +internal/static/dist/ +!internal/static/dist/.gitkeep + +# Config (sensitive) +configs/config.local.json + +# Misc +*.log +*.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c95cba4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,74 @@ +# Stage 1: Build frontend +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/web + +# Copy package files +COPY web/package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy frontend source +COPY web/ ./ + +# Build frontend +RUN npm run build + +# Stage 2: Build backend +FROM golang:1.24-alpine AS backend-builder + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go env -w GOPROXY=https://goproxy.cn,direct && go mod download + +# Copy source code +COPY . . + +# Copy built frontend to embed location +COPY --from=frontend-builder /app/web/dist ./internal/static/dist + +# Build Go binary (no CGO needed, auto-detect target arch) +ARG VERSION=dev +ARG BUILD_TIME +RUN CGO_ENABLED=0 go build -ldflags="-s -w -X baihu/internal/constant.Version=${VERSION} -X 'baihu/internal/constant.BuildTime=${BUILD_TIME}'" -o baihu . + +# Stage 3: Final image based on Dockerfile.debian +FROM debian:bookworm-slim + +ENV TZ=Asia/Shanghai +ENV CONDA_DIR=/opt/miniforge3 +ENV PATH=${CONDA_DIR}/bin:$PATH + +RUN sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources \ + && sed -i 's@security.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources \ + && echo "${TZ}" > /etc/timezone \ + && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && apt update \ + && apt install -y tzdata git gcc curl wget vim ca-certificates htop \ + && rm -rf /var/lib/apt/lists/* \ + && curl -sSL https://gh-proxy.com/https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh -o /tmp/miniforge.sh \ + && bash /tmp/miniforge.sh -b -p ${CONDA_DIR} \ + && rm -f /tmp/miniforge.sh \ + && "${CONDA_DIR}"/bin/conda config --set show_channel_urls yes \ + && "${CONDA_DIR}"/bin/conda config --set channel_priority strict \ + && "${CONDA_DIR}"/bin/conda clean -afy + +WORKDIR /app + +# Copy binary from builder +COPY --from=backend-builder /app/baihu . + +# Copy config files +COPY --from=backend-builder /app/configs ./configs + +# Create directories +RUN mkdir -p ./data ./logs ./scripts + +# Expose port +EXPOSE 8052 + +# Run +CMD ["./baihu"] diff --git a/Dockerfile.debian b/Dockerfile.debian new file mode 100644 index 0000000..b70e20c --- /dev/null +++ b/Dockerfile.debian @@ -0,0 +1,21 @@ +### debian +FROM debian:bookworm-slim + +ENV TZ=Asia/Shanghai +ENV CONDA_DIR=/opt/miniforge3 +ENV PATH=${CONDA_DIR}/bin:$PATH + +RUN sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources \ + && sed -i 's@security.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list.d/debian.sources \ + && echo "${TZ}" > /etc/timezone \ + && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && apt update \ + && apt install -y tzdata git gcc curl wget vim \ + && rm -rf /var/lib/apt/lists/* \ + + && curl -sSL https://gh-proxy.com/https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh -o /tmp/miniforge.sh \ + && bash /tmp/miniforge.sh -b -p ${CONDA_DIR} \ + && rm -f /tmp/miniforge.sh \ + && "${CONDA_DIR}"/bin/conda config --set show_channel_urls yes \ + && "${CONDA_DIR}"/bin/conda config --set channel_priority strict \ + && "${CONDA_DIR}"/bin/conda clean -afy diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8399d74 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 engigu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..513ca40 --- /dev/null +++ b/Makefile @@ -0,0 +1,77 @@ +# Variables +BINARY=baihu +GOBUILD=go build +GOCLEAN=go clean +GOGET=go get +GOMOD=go mod +VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +BUILD_TIME=$(shell date '+%Y/%m/%d %H:%M:%S') +LDFLAGS=-ldflags="-s -w -X 'baihu/internal/constant.Version=$(VERSION)' -X 'baihu/internal/constant.BuildTime=$(BUILD_TIME)'" + +# Default target +all: build + +# Build frontend +build-web: + cd web && npm ci && npm run build + rm -rf internal/static/dist + cp -r web/dist internal/static/dist + +# Build the application (requires frontend to be built first) +build: + CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o $(BINARY) main.go + +# Build all (frontend + backend) +build-all: build-web build + +# Clean built files +clean: + $(GOCLEAN) + rm -f $(BINARY) + rm -rf internal/static/dist + mkdir -p internal/static/dist + touch internal/static/dist/.gitkeep + +# Run the application +run: + $(GOBUILD) -o $(BINARY) main.go + ./$(BINARY) + +# Development run (without embedding frontend) +dev: + $(GOBUILD) -o $(BINARY) main.go + ./$(BINARY) + +# Install dependencies +deps: + $(GOMOD) tidy + +# Docker build +docker-build: + docker build -t $(BINARY) . + +# Docker run +docker-run: + docker run -p 8052:8052 $(BINARY) + +# Docker compose up +docker-up: + docker-compose up -d + +# Docker compose down +docker-down: + docker-compose down + +# Help +help: + @echo "Available targets:" + @echo " all - Build the application (default)" + @echo " build - Build the application" + @echo " clean - Clean built files" + @echo " run - Run the application" + @echo " deps - Install dependencies" + @echo " docker-build - Build Docker image" + @echo " docker-run - Run Docker container" + @echo " docker-up - Start Docker Compose stack" + @echo " docker-down - Stop Docker Compose stack" + @echo " help - Show this help message" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bed164d --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# QingLong Panel (Go Implementation) + +This is a Go implementation of the popular QingLong Panel, a task management system for automated scripts. + +## Features + +- Task scheduling and management +- RESTful API +- User authentication +- Environment variable management +- Script file management +- Task execution and logging +- Web interface +- Docker support + +## Project Structure + +``` +├── main.go # Application entry point +├── go.mod # Go modules definition +├── go.sum # Go modules checksums +├── Dockerfile # Docker configuration +├── docker-compose.yml # Docker Compose configuration +├── configs/ # Configuration files +├── internal/ +│ ├── controllers/ # HTTP handlers +│ ├── models/ # Data structures +│ ├── services/ # Business logic +│ └── utils/ # Utility functions +├── web/ +│ ├── static/ # Static assets (CSS, JS, images) +│ └── templates/ # HTML templates +├── data/ # Data storage +├── logs/ # Log files +└── scripts/ # User scripts +``` + +## Getting Started + +### Using Go directly + +1. Install Go (version 1.21 or higher) +2. Clone the repository +3. Run `go mod tidy` to install dependencies +4. Run `go run main.go` to start the server + +### Using Docker + +1. Install Docker and Docker Compose +2. Run `docker-compose up -d` to start the server + +The server will start on port 8080. + +## API Endpoints + +### Authentication +- `POST /api/auth/login` - User login +- `POST /api/auth/register` - User registration + +### Tasks +- `GET /api/tasks` - Get all tasks +- `POST /api/tasks` - Create a new task +- `GET /api/tasks/:id` - Get a specific task +- `PUT /api/tasks/:id` - Update a specific task +- `DELETE /api/tasks/:id` - Delete a specific task + +### Task Execution +- `POST /api/execute/task/:id` - Execute a task by ID +- `POST /api/execute/command` - Execute a command directly +- `GET /api/execute/results` - Get last execution results + +### Environment Variables +- `GET /api/env` - Get all environment variables +- `POST /api/env` - Create a new environment variable +- `GET /api/env/:id` - Get a specific environment variable +- `PUT /api/env/:id` - Update a specific environment variable +- `DELETE /api/env/:id` - Delete a specific environment variable + +### Scripts +- `GET /api/scripts` - Get all scripts +- `POST /api/scripts` - Create a new script +- `GET /api/scripts/:id` - Get a specific script +- `PUT /api/scripts/:id` - Update a specific script +- `DELETE /api/scripts/:id` - Delete a specific script + +## Web Interface + +- `/` - Dashboard +- `/login` - Login page +- `/tasks` - Task management +- `/scripts` - Script management +- `/environments` - Environment variable management + +## Default Admin User + +Username: `admin` +Password: `admin123` + +## Configuration + +The application can be configured using the `configs/config.json` file: + +```json +{ + "server": { + "port": 8080, + "host": "0.0.0.0" + }, + "database": { + "type": "sqlite", + "path": "./data/ql.db" + }, + "security": { + "jwt_secret": "ql_panel_secret_key", + "password_salt": "ql_panel_salt" + }, + "task": { + "default_timeout": 3600, + "log_retention_days": 30 + } +} +``` + +## Docker Support + +The application includes Docker support for easy deployment: + +1. Build the Docker image: `docker build -t baihu .` +2. Run the container: `docker run -p 8080:8080 baihu` + +Or use Docker Compose: +```bash +docker-compose up -d +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file diff --git a/configs/config.json b/configs/config.json new file mode 100644 index 0000000..44ef8a5 --- /dev/null +++ b/configs/config.json @@ -0,0 +1,24 @@ +{ + "server": { + "port": 8052, + "host": "0.0.0.0" + }, + "database": { + "type": "sqlite", + "host": "localhost", + "port": 3306, + "user": "root", + "password": "", + "dbname": "ql_panel", + "path": "./data/ql.db", + "table_prefix": "baihu_" + }, + "security": { + "jwt_secret": "ql_panel_secret_key", + "password_salt": "ql_panel_salt" + }, + "task": { + "default_timeout": 3600, + "log_retention_days": 30 + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..451804d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + baihu: + build: . + ports: + - "8052:8052" + volumes: + - ./data:/root/data + - ./logs:/root/logs + - ./scripts:/root/scripts + - ./configs:/root/configs + environment: + - TZ=Asia/Shanghai + restart: unless-stopped \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..01b59ce --- /dev/null +++ b/go.mod @@ -0,0 +1,64 @@ +module baihu + +go 1.24.0 + +toolchain go1.24.7 + +require ( + github.com/creack/pty v1.1.24 + github.com/gin-gonic/gin v1.9.1 + github.com/glebarez/sqlite v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/gorilla/websocket v1.5.3 + github.com/robfig/cron/v3 v3.0.1 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/text v0.32.0 + gorm.io/driver/mysql v1.6.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..58cee1c --- /dev/null +++ b/go.sum @@ -0,0 +1,151 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go new file mode 100644 index 0000000..8c4fb4f --- /dev/null +++ b/internal/bootstrap/bootstrap.go @@ -0,0 +1,76 @@ +package bootstrap + +import ( + "fmt" + "os" + + "baihu/internal/constant" + "baihu/internal/database" + "baihu/internal/logger" + "baihu/internal/router" + "baihu/internal/services" + + "github.com/gin-gonic/gin" +) + +type App struct { + Config *services.AppConfig + Router *gin.Engine +} + +func New() *App { + app := &App{} + app.initConfig() + app.initDatabase() + app.initRouter() + return app +} + +func (a *App) initConfig() { + cfg, err := services.LoadConfig(constant.ConfigPath) + if err != nil { + logger.Fatalf("Failed to load config: %v", err) + } + a.Config = cfg + + // Ensure directories exist + err = os.MkdirAll(constant.DataDir, 0755) + if err != nil { + return + } + err = os.MkdirAll(constant.ScriptsWorkDir, 0755) + if err != nil { + return + } +} + +func (a *App) initDatabase() { + dbCfg := &database.Config{ + Type: a.Config.Database.Type, + Host: a.Config.Database.Host, + Port: a.Config.Database.Port, + User: a.Config.Database.User, + Password: a.Config.Database.Password, + DBName: a.Config.Database.DBName, + Path: a.Config.Database.Path, + } + + if err := database.Init(dbCfg); err != nil { + logger.Fatalf("Failed to init database: %v", err) + } + + if err := database.Migrate(); err != nil { + logger.Fatalf("Failed to migrate database: %v", err) + } +} + +func (a *App) initRouter() { + ctrls := router.RegisterControllers() + a.Router = router.Setup(ctrls) +} + +func (a *App) Run() { + addr := fmt.Sprintf("%s:%d", a.Config.Server.Host, a.Config.Server.Port) + logger.Infof("Starting server on %s", addr) + a.Router.Run(addr) +} diff --git a/internal/constant/constant.go b/internal/constant/constant.go new file mode 100644 index 0000000..2c249cf --- /dev/null +++ b/internal/constant/constant.go @@ -0,0 +1,48 @@ +package constant + +const ( + + // ConfigPath 配置文件路径 + ConfigPath = "configs/config.json" + + // DataDir 数据目录 + DataDir = "./data" + + // WebDistDir 前端构建目录 + WebDistDir = "./web/dist" + + // DefaultRole 默认用户角色 + DefaultRole = "user" + + // AdminRole 管理员角色 + AdminRole = "admin" + + // DefaultTablePrefix 默认表前缀 + DefaultTablePrefix = "baihu_" + + // ScriptsWorkDir 脚本工作目录 + ScriptsWorkDir = "./data/scripts" + + // DefaultPageSize 默认分页大小 + DefaultPageSize = 10 + + // CookieName Cookie 名称 + CookieName = "BHToken" + + // TokenExpireDays Token 过期天数 + TokenExpireDays = 7 + // CookieMaxAge Cookie 有效期(秒)7天 + CookieMaxAge = 86400 * TokenExpireDays + + // DefaultJWTSecret 默认 JWT 密钥 + DefaultJWTSecret = "baihu-default-secret-key" + + // DefaultTaskTimeout 默认任务超时时间(分钟) + DefaultTaskTimeout = 30 +) + +// TablePrefix 表前缀,可在运行时设置 +var TablePrefix = DefaultTablePrefix + +// JWTSecret JWT 密钥,可通过配置文件设置 +var JWTSecret = DefaultJWTSecret diff --git a/internal/constant/version.go b/internal/constant/version.go new file mode 100644 index 0000000..8cf689b --- /dev/null +++ b/internal/constant/version.go @@ -0,0 +1,12 @@ +package constant + +import "time" + +// 构建时注入的变量 +var ( + Version = "dev" + BuildTime = "unknown" +) + +// 程序启动时间 +var StartTime = time.Now() diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go new file mode 100644 index 0000000..4e9ee5f --- /dev/null +++ b/internal/controllers/auth_controller.go @@ -0,0 +1,81 @@ +package controllers + +import ( + "baihu/internal/middleware" + "baihu/internal/services" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type AuthController struct { + userService *services.UserService +} + +func NewAuthController(userService *services.UserService) *AuthController { + return &AuthController{userService: userService} +} + +func (ac *AuthController) Login(c *gin.Context) { + var req struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + user := ac.userService.GetUserByUsername(req.Username) + if user == nil || !ac.userService.ValidatePassword(user, req.Password) { + utils.Unauthorized(c, "用户名或密码错误") + return + } + + // 生成 token + token, err := utils.GenerateToken(user.ID, user.Username) + if err != nil { + utils.ServerError(c, "登录失败") + return + } + + // 设置 Cookie + middleware.SetAuthCookie(c, token) + + utils.Success(c, gin.H{ + "user": user.Username, + }) +} + +func (ac *AuthController) Logout(c *gin.Context) { + middleware.ClearAuthCookie(c) + utils.SuccessMsg(c, "退出成功") +} + +func (ac *AuthController) GetCurrentUser(c *gin.Context) { + username, exists := c.Get("username") + if !exists { + utils.Unauthorized(c, "未登录") + return + } + utils.Success(c, gin.H{ + "username": username, + }) +} + +func (ac *AuthController) Register(c *gin.Context) { + var req struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + user := ac.userService.CreateUser(req.Username, req.Email, req.Password, "user") + utils.Success(c, user) +} diff --git a/internal/controllers/dashboard_controller.go b/internal/controllers/dashboard_controller.go new file mode 100644 index 0000000..65df6e3 --- /dev/null +++ b/internal/controllers/dashboard_controller.go @@ -0,0 +1,51 @@ +package controllers + +import ( + "baihu/internal/database" + "baihu/internal/models" + "baihu/internal/services" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type DashboardController struct { + cronService *services.CronService + executorService *services.ExecutorService +} + +func NewDashboardController(cronService *services.CronService, executorService *services.ExecutorService) *DashboardController { + return &DashboardController{ + cronService: cronService, + executorService: executorService, + } +} + +type StatsResponse struct { + Tasks int64 `json:"tasks"` + Scripts int64 `json:"scripts"` + Envs int64 `json:"envs"` + Logs int64 `json:"logs"` + Scheduled int `json:"scheduled"` + Running int `json:"running"` +} + +func (dc *DashboardController) GetStats(c *gin.Context) { + var taskCount, scriptCount, envCount, logCount int64 + + database.DB.Model(&models.Task{}).Count(&taskCount) + database.DB.Model(&models.Script{}).Count(&scriptCount) + database.DB.Model(&models.EnvironmentVariable{}).Count(&envCount) + database.DB.Model(&models.TaskLog{}).Count(&logCount) + + stats := StatsResponse{ + Tasks: taskCount, + Scripts: scriptCount, + Envs: envCount, + Logs: logCount, + Scheduled: dc.cronService.GetScheduledCount(), + Running: dc.executorService.GetRunningCount(), + } + + utils.Success(c, stats) +} diff --git a/internal/controllers/env_controller.go b/internal/controllers/env_controller.go new file mode 100644 index 0000000..57a53df --- /dev/null +++ b/internal/controllers/env_controller.go @@ -0,0 +1,103 @@ +package controllers + +import ( + "strconv" + + "baihu/internal/services" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type EnvController struct { + envService *services.EnvService +} + +func NewEnvController(envService *services.EnvService) *EnvController { + return &EnvController{envService: envService} +} + +func (ec *EnvController) CreateEnvVar(c *gin.Context) { + userID := 1 + + var req struct { + Name string `json:"name" binding:"required"` + Value string `json:"value" binding:"required"` + Remark string `json:"remark"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + envVar := ec.envService.CreateEnvVar(req.Name, req.Value, req.Remark, userID) + utils.Success(c, envVar) +} + +func (ec *EnvController) GetEnvVars(c *gin.Context) { + userID := 1 + p := utils.ParsePagination(c) + name := c.DefaultQuery("name", "") + envVars, total := ec.envService.GetEnvVarsWithPagination(userID, name, p.Page, p.PageSize) + utils.PaginatedResponse(c, envVars, total, p) +} + +func (ec *EnvController) GetEnvVar(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的环境变量ID") + return + } + + envVar := ec.envService.GetEnvVarByID(id) + if envVar == nil { + utils.NotFound(c, "环境变量不存在") + return + } + + utils.Success(c, envVar) +} + +func (ec *EnvController) UpdateEnvVar(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的环境变量ID") + return + } + + var req struct { + Name string `json:"name"` + Value string `json:"value"` + Remark string `json:"remark"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + envVar := ec.envService.UpdateEnvVar(id, req.Name, req.Value, req.Remark) + if envVar == nil { + utils.NotFound(c, "环境变量不存在") + return + } + + utils.Success(c, envVar) +} + +func (ec *EnvController) DeleteEnvVar(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的环境变量ID") + return + } + + success := ec.envService.DeleteEnvVar(id) + if !success { + utils.NotFound(c, "环境变量不存在") + return + } + + utils.SuccessMsg(c, "删除成功") +} diff --git a/internal/controllers/executor_controller.go b/internal/controllers/executor_controller.go new file mode 100644 index 0000000..3b8b7f7 --- /dev/null +++ b/internal/controllers/executor_controller.go @@ -0,0 +1,55 @@ +package controllers + +import ( + "strconv" + + "baihu/internal/services" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type ExecutorController struct { + executorService *services.ExecutorService +} + +func NewExecutorController(executorService *services.ExecutorService) *ExecutorController { + return &ExecutorController{executorService: executorService} +} + +func (ec *ExecutorController) ExecuteTask(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的任务ID") + return + } + + result := ec.executorService.ExecuteTask(id) + utils.Success(c, result) +} + +func (ec *ExecutorController) ExecuteCommand(c *gin.Context) { + var req struct { + Command string `json:"command" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + result := ec.executorService.ExecuteCommand(req.Command) + utils.Success(c, result) +} + +func (ec *ExecutorController) GetLastResults(c *gin.Context) { + count := 10 + if c.Query("count") != "" { + if parsedCount, err := strconv.Atoi(c.Query("count")); err == nil && parsedCount > 0 { + count = parsedCount + } + } + + results := ec.executorService.GetLastResults(count) + utils.Success(c, results) +} diff --git a/internal/controllers/file_controller.go b/internal/controllers/file_controller.go new file mode 100644 index 0000000..260510d --- /dev/null +++ b/internal/controllers/file_controller.go @@ -0,0 +1,344 @@ +package controllers + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +var ( + extractZip = utils.ExtractZip + extractTar = utils.ExtractTar + extractTarGz = utils.ExtractTarGz +) + +type FileController struct { + workDir string +} + +func NewFileController(workDir string) *FileController { + os.MkdirAll(workDir, 0755) + absPath, err := filepath.Abs(workDir) + if err != nil { + absPath = workDir + } + return &FileController{workDir: absPath} +} + +type FileNode struct { + Name string `json:"name"` + Path string `json:"path"` + IsDir bool `json:"isDir"` + Children []*FileNode `json:"children,omitempty"` +} + +func (fc *FileController) GetFileTree(c *gin.Context) { + root := &FileNode{ + Name: filepath.Base(fc.workDir), + Path: "", + IsDir: true, + Children: []*FileNode{}, + } + + err := filepath.WalkDir(fc.workDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + if path == fc.workDir { + return nil + } + + relPath, _ := filepath.Rel(fc.workDir, path) + parts := strings.Split(relPath, string(filepath.Separator)) + + current := root + for i, part := range parts { + found := false + for _, child := range current.Children { + if child.Name == part { + current = child + found = true + break + } + } + if !found { + isLast := i == len(parts)-1 + isDir := !isLast || d.IsDir() + node := &FileNode{ + Name: part, + Path: strings.Join(parts[:i+1], "/"), + IsDir: isDir, + } + if isDir { + node.Children = []*FileNode{} + } + current.Children = append(current.Children, node) + current = node + } + } + return nil + }) + + if err != nil { + utils.ServerError(c, err.Error()) + return + } + + utils.Success(c, root.Children) +} + +func (fc *FileController) GetFileContent(c *gin.Context) { + filePath := c.Query("path") + if filePath == "" { + utils.BadRequest(c, "path参数必填") + return + } + + fullPath := filepath.Join(fc.workDir, filepath.Clean(filePath)) + if !strings.HasPrefix(fullPath, fc.workDir) { + utils.Forbidden(c, "访问被拒绝") + return + } + + content, err := os.ReadFile(fullPath) + if err != nil { + utils.NotFound(c, "文件不存在") + return + } + + utils.Success(c, gin.H{ + "path": filePath, + "content": string(content), + }) +} + +func (fc *FileController) SaveFileContent(c *gin.Context) { + var req struct { + Path string `json:"path" binding:"required"` + Content string `json:"content"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + fullPath := filepath.Join(fc.workDir, filepath.Clean(req.Path)) + if !strings.HasPrefix(fullPath, fc.workDir) { + utils.Forbidden(c, "访问被拒绝") + return + } + + os.MkdirAll(filepath.Dir(fullPath), 0755) + + if err := os.WriteFile(fullPath, []byte(req.Content), 0644); err != nil { + utils.ServerError(c, err.Error()) + return + } + + utils.SuccessMsg(c, "保存成功") +} + +func (fc *FileController) CreateFile(c *gin.Context) { + var req struct { + Path string `json:"path" binding:"required"` + IsDir bool `json:"isDir"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + fullPath := filepath.Join(fc.workDir, filepath.Clean(req.Path)) + if !strings.HasPrefix(fullPath, fc.workDir) { + utils.Forbidden(c, "访问被拒绝") + return + } + + if req.IsDir { + if err := os.MkdirAll(fullPath, 0755); err != nil { + utils.ServerError(c, err.Error()) + return + } + } else { + os.MkdirAll(filepath.Dir(fullPath), 0755) + if err := os.WriteFile(fullPath, []byte(""), 0644); err != nil { + utils.ServerError(c, err.Error()) + return + } + } + + utils.SuccessMsg(c, "创建成功") +} + +func (fc *FileController) DeleteFile(c *gin.Context) { + var req struct { + Path string `json:"path" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + fullPath := filepath.Join(fc.workDir, filepath.Clean(req.Path)) + if !strings.HasPrefix(fullPath, fc.workDir) { + utils.Forbidden(c, "访问被拒绝") + return + } + + if err := os.RemoveAll(fullPath); err != nil { + utils.ServerError(c, err.Error()) + return + } + + utils.SuccessMsg(c, "删除成功") +} + +func (fc *FileController) RenameFile(c *gin.Context) { + var req struct { + OldPath string `json:"oldPath" binding:"required"` + NewPath string `json:"newPath" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + oldFull := filepath.Join(fc.workDir, filepath.Clean(req.OldPath)) + newFull := filepath.Join(fc.workDir, filepath.Clean(req.NewPath)) + + if !strings.HasPrefix(oldFull, fc.workDir) || !strings.HasPrefix(newFull, fc.workDir) { + utils.Forbidden(c, "访问被拒绝") + return + } + + // 确保目标目录存在 + os.MkdirAll(filepath.Dir(newFull), 0755) + + if err := os.Rename(oldFull, newFull); err != nil { + utils.ServerError(c, err.Error()) + return + } + + utils.SuccessMsg(c, "移动成功") +} + +// UploadArchive handles archive file upload and extraction +func (fc *FileController) UploadArchive(c *gin.Context) { + targetDir := c.PostForm("path") + + file, err := c.FormFile("file") + if err != nil { + utils.BadRequest(c, "请选择文件") + return + } + + // 检查文件类型 + ext := strings.ToLower(filepath.Ext(file.Filename)) + if ext != ".zip" && ext != ".tar" && ext != ".gz" && ext != ".tgz" { + utils.BadRequest(c, "仅支持 zip、tar、gz、tgz 格式") + return + } + + // 确定解压目标目录 + extractDir := fc.workDir + if targetDir != "" { + extractDir = filepath.Join(fc.workDir, filepath.Clean(targetDir)) + if !strings.HasPrefix(extractDir, fc.workDir) { + utils.Forbidden(c, "访问被拒绝") + return + } + } + os.MkdirAll(extractDir, 0755) + + // 保存临时文件 + tempFile := filepath.Join(os.TempDir(), file.Filename) + if err := c.SaveUploadedFile(file, tempFile); err != nil { + utils.ServerError(c, "保存文件失败") + return + } + defer os.Remove(tempFile) + + // 解压文件 + var extractErr error + switch { + case ext == ".zip": + extractErr = extractZip(tempFile, extractDir) + case ext == ".tar": + extractErr = extractTar(tempFile, extractDir) + case ext == ".gz" || ext == ".tgz": + extractErr = extractTarGz(tempFile, extractDir) + } + + if extractErr != nil { + utils.ServerError(c, "解压失败: "+extractErr.Error()) + return + } + + utils.SuccessMsg(c, "导入成功") +} + +// UploadFiles handles multiple file uploads +func (fc *FileController) UploadFiles(c *gin.Context) { + targetDir := c.PostForm("path") + + // 确定目标目录 + destDir := fc.workDir + if targetDir != "" { + destDir = filepath.Join(fc.workDir, filepath.Clean(targetDir)) + if !strings.HasPrefix(destDir, fc.workDir) { + utils.Forbidden(c, "访问被拒绝") + return + } + } + os.MkdirAll(destDir, 0755) + + form, err := c.MultipartForm() + if err != nil { + utils.BadRequest(c, "请选择文件") + return + } + + files := form.File["files"] + paths := form.Value["paths"] // 相对路径数组,用于保持文件夹结构 + + if len(files) == 0 { + utils.BadRequest(c, "请选择文件") + return + } + + for i, file := range files { + // 获取相对路径(如果有) + relPath := file.Filename + if i < len(paths) && paths[i] != "" { + relPath = paths[i] + } + + // 构建完整路径 + fullPath := filepath.Join(destDir, filepath.Clean(relPath)) + + // 安全检查 + if !strings.HasPrefix(fullPath, fc.workDir) { + continue + } + + // 确保父目录存在 + os.MkdirAll(filepath.Dir(fullPath), 0755) + + // 保存文件 + if err := c.SaveUploadedFile(file, fullPath); err != nil { + utils.ServerError(c, "保存文件失败: "+err.Error()) + return + } + } + + utils.SuccessMsg(c, "上传成功") +} diff --git a/internal/controllers/log_controller.go b/internal/controllers/log_controller.go new file mode 100644 index 0000000..e9a6403 --- /dev/null +++ b/internal/controllers/log_controller.go @@ -0,0 +1,107 @@ +package controllers + +import ( + "strconv" + + "baihu/internal/database" + "baihu/internal/models" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type LogController struct{} + +func NewLogController() *LogController { + return &LogController{} +} + +type TaskLogResponse struct { + ID uint `json:"id"` + TaskID uint `json:"task_id"` + TaskName string `json:"task_name"` + Command string `json:"command"` + Status string `json:"status"` + Duration int64 `json:"duration"` + CreatedAt models.LocalTime `json:"created_at"` +} + +func (lc *LogController) GetLogs(c *gin.Context) { + p := utils.ParsePagination(c) + taskID, _ := strconv.Atoi(c.DefaultQuery("task_id", "0")) + taskName := c.DefaultQuery("task_name", "") + + var logs []models.TaskLog + var total int64 + + query := database.DB.Model(&models.TaskLog{}) + if taskID > 0 { + query = query.Where("task_id = ?", taskID) + } + + // 按任务名称过滤 + if taskName != "" { + var taskIDs []uint + database.DB.Model(&models.Task{}).Where("name LIKE ?", "%"+taskName+"%").Pluck("id", &taskIDs) + if len(taskIDs) > 0 { + query = query.Where("task_id IN ?", taskIDs) + } else { + utils.PaginatedResponse(c, []TaskLogResponse{}, 0, p) + return + } + } + + query.Count(&total) + query.Order("id DESC").Offset(p.Offset()).Limit(p.PageSize).Find(&logs) + + taskIDList := make([]uint, 0) + for _, log := range logs { + taskIDList = append(taskIDList, log.TaskID) + } + + var tasks []models.Task + database.DB.Where("id IN ?", taskIDList).Find(&tasks) + taskMap := make(map[uint]string) + for _, t := range tasks { + taskMap[t.ID] = t.Name + } + + result := make([]TaskLogResponse, len(logs)) + for i, log := range logs { + result[i] = TaskLogResponse{ + ID: log.ID, + TaskID: log.TaskID, + TaskName: taskMap[log.TaskID], + Command: log.Command, + Status: log.Status, + Duration: log.Duration, + CreatedAt: log.CreatedAt, + } + } + + utils.PaginatedResponse(c, result, total, p) +} + +func (lc *LogController) GetLogDetail(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的日志ID") + return + } + + var log models.TaskLog + if err := database.DB.First(&log, id).Error; err != nil { + utils.NotFound(c, "日志不存在") + return + } + + utils.Success(c, gin.H{ + "id": log.ID, + "task_id": log.TaskID, + "command": log.Command, + "output": log.Output, + "status": log.Status, + "duration": log.Duration, + "created_at": log.CreatedAt, + }) +} diff --git a/internal/controllers/script_controller.go b/internal/controllers/script_controller.go new file mode 100644 index 0000000..52d864e --- /dev/null +++ b/internal/controllers/script_controller.go @@ -0,0 +1,99 @@ +package controllers + +import ( + "strconv" + + "baihu/internal/services" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type ScriptController struct { + scriptService *services.ScriptService +} + +func NewScriptController(scriptService *services.ScriptService) *ScriptController { + return &ScriptController{scriptService: scriptService} +} + +func (sc *ScriptController) CreateScript(c *gin.Context) { + userID := 1 + + var req struct { + Name string `json:"name" binding:"required"` + Content string `json:"content" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + script := sc.scriptService.CreateScript(req.Name, req.Content, userID) + utils.Success(c, script) +} + +func (sc *ScriptController) GetScripts(c *gin.Context) { + userID := 1 + scripts := sc.scriptService.GetScriptsByUserID(userID) + utils.Success(c, scripts) +} + +func (sc *ScriptController) GetScript(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的脚本ID") + return + } + + script := sc.scriptService.GetScriptByID(id) + if script == nil { + utils.NotFound(c, "脚本不存在") + return + } + + utils.Success(c, script) +} + +func (sc *ScriptController) UpdateScript(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的脚本ID") + return + } + + var req struct { + Name string `json:"name"` + Content string `json:"content"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + script := sc.scriptService.UpdateScript(id, req.Name, req.Content) + if script == nil { + utils.NotFound(c, "脚本不存在") + return + } + + utils.Success(c, script) +} + +func (sc *ScriptController) DeleteScript(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的脚本ID") + return + } + + success := sc.scriptService.DeleteScript(id) + if !success { + utils.NotFound(c, "脚本不存在") + return + } + + utils.SuccessMsg(c, "删除成功") +} diff --git a/internal/controllers/settings_controller.go b/internal/controllers/settings_controller.go new file mode 100644 index 0000000..3056791 --- /dev/null +++ b/internal/controllers/settings_controller.go @@ -0,0 +1,149 @@ +package controllers + +import ( + "baihu/internal/constant" + "baihu/internal/database" + "baihu/internal/models" + "baihu/internal/services" + "baihu/internal/utils" + "fmt" + "runtime" + "time" + + "github.com/gin-gonic/gin" +) + +type SettingsController struct { + userService *services.UserService +} + +func NewSettingsController(userService *services.UserService) *SettingsController { + return &SettingsController{userService: userService} +} + +// ChangePassword 修改密码 +func (sc *SettingsController) ChangePassword(c *gin.Context) { + var req struct { + OldPassword string `json:"old_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=6"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, "参数错误") + return + } + + // 暂时使用固定用户名 admin + user := sc.userService.GetUserByUsername("admin") + if user == nil { + utils.NotFound(c, "用户不存在") + return + } + + if !sc.userService.ValidatePassword(user, req.OldPassword) { + utils.BadRequest(c, "原密码错误") + return + } + + if err := sc.userService.UpdatePassword(user.ID, req.NewPassword); err != nil { + utils.ServerError(c, "修改密码失败") + return + } + + utils.SuccessMsg(c, "密码修改成功") +} + +// CleanLogs 清理日志 +func (sc *SettingsController) CleanLogs(c *gin.Context) { + var req struct { + Days int `json:"days" binding:"required,min=1"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, "参数错误") + return + } + + cutoff := time.Now().AddDate(0, 0, -req.Days) + result := database.DB.Where("created_at < ?", cutoff).Delete(&models.TaskLog{}) + + utils.Success(c, gin.H{ + "deleted": result.RowsAffected, + }) +} + +// GetSiteSettings 获取站点设置 +func (sc *SettingsController) GetSiteSettings(c *gin.Context) { + config := services.Config + if config == nil { + utils.Success(c, gin.H{ + "site_name": "白虎面板", + "port": 8080, + }) + return + } + + utils.Success(c, gin.H{ + "site_name": config.Server.SiteName, + "port": config.Server.Port, + }) +} + +// GetAbout 获取关于信息 +func (sc *SettingsController) GetAbout(c *gin.Context) { + var taskCount, logCount, envCount int64 + database.DB.Model(&models.Task{}).Count(&taskCount) + database.DB.Model(&models.TaskLog{}).Count(&logCount) + database.DB.Model(&models.EnvironmentVariable{}).Count(&envCount) + + // 内存使用 + var m runtime.MemStats + runtime.ReadMemStats(&m) + memUsage := formatBytes(m.Alloc) + + // 运行时间 + uptime := formatDuration(time.Since(constant.StartTime)) + + utils.Success(c, gin.H{ + "version": constant.Version, + "build_time": constant.BuildTime, + "mem_usage": memUsage, + "uptime": uptime, + "task_count": taskCount, + "log_count": logCount, + "env_count": envCount, + }) +} + +// formatBytes 格式化字节数 +func formatBytes(bytes uint64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := uint64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +// formatDuration 格式化时间间隔 +func formatDuration(d time.Duration) string { + days := int(d.Hours()) / 24 + hours := int(d.Hours()) % 24 + minutes := int(d.Minutes()) % 60 + seconds := int(d.Seconds()) % 60 + + if days > 0 { + return fmt.Sprintf("%d天%d小时%d分钟%d秒", days, hours, minutes, seconds) + } + if hours > 0 { + return fmt.Sprintf("%d小时%d分钟%d秒", hours, minutes, seconds) + } + if minutes > 0 { + return fmt.Sprintf("%d分钟%d秒", minutes, seconds) + } + return fmt.Sprintf("%d秒", seconds) +} diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go new file mode 100644 index 0000000..5ceb34f --- /dev/null +++ b/internal/controllers/task_controller.go @@ -0,0 +1,128 @@ +package controllers + +import ( + "strconv" + + "baihu/internal/services" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +type TaskController struct { + taskService *services.TaskService + cronService *services.CronService +} + +func NewTaskController(taskService *services.TaskService, cronService *services.CronService) *TaskController { + return &TaskController{ + taskService: taskService, + cronService: cronService, + } +} + +func (tc *TaskController) CreateTask(c *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + Command string `json:"command" binding:"required"` + Schedule string `json:"schedule" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + if err := tc.cronService.ValidateCron(req.Schedule); err != nil { + utils.BadRequest(c, "无效的cron表达式: "+err.Error()) + return + } + + task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule) + tc.cronService.AddTask(task) + + utils.Success(c, task) +} + +func (tc *TaskController) GetTasks(c *gin.Context) { + p := utils.ParsePagination(c) + name := c.DefaultQuery("name", "") + + tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name) + utils.PaginatedResponse(c, tasks, total, p) +} + +func (tc *TaskController) GetTask(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的任务ID") + return + } + + task := tc.taskService.GetTaskByID(id) + if task == nil { + utils.NotFound(c, "任务不存在") + return + } + + utils.Success(c, task) +} + +func (tc *TaskController) UpdateTask(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的任务ID") + return + } + + var req struct { + Name string `json:"name"` + Command string `json:"command"` + Schedule string `json:"schedule"` + Enabled bool `json:"enabled"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + if req.Schedule != "" { + if err := tc.cronService.ValidateCron(req.Schedule); err != nil { + utils.BadRequest(c, "无效的cron表达式: "+err.Error()) + return + } + } + + task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Enabled) + if task == nil { + utils.NotFound(c, "任务不存在") + return + } + + if task.Enabled { + tc.cronService.AddTask(task) + } else { + tc.cronService.RemoveTask(task.ID) + } + + utils.Success(c, task) +} + +func (tc *TaskController) DeleteTask(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的任务ID") + return + } + + tc.cronService.RemoveTask(uint(id)) + + success := tc.taskService.DeleteTask(id) + if !success { + utils.NotFound(c, "任务不存在") + return + } + + utils.SuccessMsg(c, "删除成功") +} diff --git a/internal/controllers/terminal_controller.go b/internal/controllers/terminal_controller.go new file mode 100644 index 0000000..6400280 --- /dev/null +++ b/internal/controllers/terminal_controller.go @@ -0,0 +1,249 @@ +package controllers + +import ( + "bufio" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "sync" + "unicode/utf8" + + "baihu/internal/constant" + "baihu/internal/utils" + + "github.com/creack/pty" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/transform" +) + +type TerminalController struct{} + +func NewTerminalController() *TerminalController { + return &TerminalController{} +} + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// toUTF8 将可能是 GBK 编码的字节转换为 UTF-8 +func toUTF8(data []byte) string { + if utf8.Valid(data) { + return string(data) + } + // 尝试从 GBK 转换 + reader := transform.NewReader( + bufio.NewReader( + &byteReader{data: data}, + ), + simplifiedchinese.GBK.NewDecoder(), + ) + result, err := io.ReadAll(reader) + if err != nil { + return string(data) + } + return string(result) +} + +type byteReader struct { + data []byte + pos int +} + +func (r *byteReader) Read(p []byte) (n int, err error) { + if r.pos >= len(r.data) { + return 0, io.EOF + } + n = copy(p, r.data[r.pos:]) + r.pos += n + return n, nil +} + +func (tc *TerminalController) HandleWebSocket(c *gin.Context) { + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + defer conn.Close() + + // Windows 使用 pipe 模式,Unix 使用 PTY 模式 + if runtime.GOOS == "windows" { + tc.handlePipeMode(conn) + } else { + tc.handlePtyMode(conn) + } +} + +// handlePtyMode 使用 PTY 处理终端(Unix/macOS) +func (tc *TerminalController) handlePtyMode(conn *websocket.Conn) { + // 发送 PTY 模式标识 + conn.WriteMessage(websocket.TextMessage, []byte("__PTY_MODE__")) + + cmd := utils.NewShellCmd() + + if absDir, err := filepath.Abs(constant.ScriptsWorkDir); err == nil { + cmd.Dir = absDir + } + + cmd.Env = append(os.Environ(), "TERM=xterm-256color") + + ptmx, err := pty.Start(cmd) + if err != nil { + conn.WriteMessage(websocket.TextMessage, []byte("Error starting shell: "+err.Error())) + return + } + defer ptmx.Close() + + pty.Setsize(ptmx, &pty.Winsize{Rows: 24, Cols: 80}) + + var wg sync.WaitGroup + var connMu sync.Mutex + + writeMessage := func(data []byte) { + connMu.Lock() + defer connMu.Unlock() + conn.WriteMessage(websocket.TextMessage, data) + } + + wg.Add(1) + go func() { + defer wg.Done() + buf := make([]byte, 4096) + for { + n, err := ptmx.Read(buf) + if err != nil { + return + } + if n > 0 { + text := toUTF8(buf[:n]) + writeMessage([]byte(text)) + } + } + }() + + for { + _, message, err := conn.ReadMessage() + if err != nil { + break + } + if _, err := ptmx.Write(message); err != nil { + break + } + } + + cmd.Process.Kill() + cmd.Wait() + wg.Wait() +} + +// handlePipeMode 使用 pipe 处理终端(Windows) +func (tc *TerminalController) handlePipeMode(conn *websocket.Conn) { + // 发送 pipe 模式标识 + conn.WriteMessage(websocket.TextMessage, []byte("__PIPE_MODE__")) + + cmd := utils.NewShellCmd() + + if absDir, err := filepath.Abs(constant.ScriptsWorkDir); err == nil { + cmd.Dir = absDir + } + + stdin, err := cmd.StdinPipe() + if err != nil { + conn.WriteMessage(websocket.TextMessage, []byte("Error: "+err.Error())) + return + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + conn.WriteMessage(websocket.TextMessage, []byte("Error: "+err.Error())) + return + } + + stderr, err := cmd.StderrPipe() + if err != nil { + conn.WriteMessage(websocket.TextMessage, []byte("Error: "+err.Error())) + return + } + + if err := cmd.Start(); err != nil { + conn.WriteMessage(websocket.TextMessage, []byte("Error: "+err.Error())) + return + } + + var wg sync.WaitGroup + var connMu sync.Mutex + + writeMessage := func(data []byte) { + connMu.Lock() + defer connMu.Unlock() + conn.WriteMessage(websocket.TextMessage, data) + } + + readOutput := func(reader io.Reader) { + defer wg.Done() + defer func() { recover() }() + buf := make([]byte, 4096) + for { + n, err := reader.Read(buf) + if err != nil { + return + } + if n > 0 { + text := toUTF8(buf[:n]) + writeMessage([]byte(text)) + } + } + } + + wg.Add(2) + go readOutput(stdout) + go readOutput(stderr) + + for { + _, message, err := conn.ReadMessage() + if err != nil { + break + } + if _, err := stdin.Write(message); err != nil { + break + } + } + + stdin.Close() + cmd.Process.Kill() + cmd.Wait() + wg.Wait() +} + +// ExecuteShellCommand 执行单个命令并返回结果 +func (tc *TerminalController) ExecuteShellCommand(c *gin.Context) { + var req struct { + Command string `json:"command" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + cmd := utils.NewShellCommandCmd(req.Command) + output, err := cmd.CombinedOutput() + + if err != nil { + utils.Success(c, gin.H{ + "output": string(output), + "error": err.Error(), + }) + return + } + + utils.Success(c, gin.H{ + "output": string(output), + }) +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..e29f6fa --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,74 @@ +package database + +import ( + "fmt" + "time" + + "baihu/internal/logger" + + "github.com/glebarez/sqlite" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +type Config struct { + Type string // sqlite, mysql, postgres + Host string + Port int + User string + Password string + DBName string + Path string // for sqlite +} + +func Init(cfg *Config) error { + // 设置东八区时区 + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + logger.Warnf("Failed to load timezone, using UTC: %v", err) + loc = time.UTC + } + time.Local = loc + + var dialector gorm.Dialector + + switch cfg.Type { + case "sqlite": + dialector = sqlite.Open(cfg.Path) + case "mysql": + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Asia%%2FShanghai", + cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName) + dialector = mysql.Open(dsn) + case "postgres": + dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai", + cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName) + dialector = postgres.Open(dsn) + default: + return fmt.Errorf("unsupported database type: %s", cfg.Type) + } + + DB, err = gorm.Open(dialector, &gorm.Config{ + Logger: gormlogger.Default.LogMode(gormlogger.Warn), + NowFunc: func() time.Time { + return time.Now().In(loc) + }, + }) + if err != nil { + return fmt.Errorf("failed to connect database: %w", err) + } + + logger.Infof("Connected to %s database with Asia/Shanghai timezone", cfg.Type) + return nil +} + +func AutoMigrate(models ...interface{}) error { + return DB.AutoMigrate(models...) +} + +func GetDB() *gorm.DB { + return DB +} diff --git a/internal/database/migrate.go b/internal/database/migrate.go new file mode 100644 index 0000000..ab47ad5 --- /dev/null +++ b/internal/database/migrate.go @@ -0,0 +1,16 @@ +package database + +import ( + "baihu/internal/models" +) + +func Migrate() error { + return AutoMigrate( + &models.User{}, + &models.Task{}, + &models.TaskLog{}, + &models.Script{}, + &models.EnvironmentVariable{}, + &models.Setting{}, + ) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..4e7cb33 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,83 @@ +package logger + +import ( + "os" + "path/filepath" + "time" + + "github.com/sirupsen/logrus" +) + +var Log *logrus.Logger + +func init() { + Log = logrus.New() + + // 设置日志格式 + Log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + ForceColors: true, + }) + + // 设置日志级别 + Log.SetLevel(logrus.InfoLevel) + + // 输出到标准输出 + Log.SetOutput(os.Stdout) +} + +// SetupFileOutput 设置文件输出 +func SetupFileOutput(logDir string) error { + if err := os.MkdirAll(logDir, 0755); err != nil { + return err + } + + logFile := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log") + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return err + } + + Log.SetOutput(file) + return nil +} + +// SetLevel 设置日志级别 +func SetLevel(level string) { + switch level { + case "debug": + Log.SetLevel(logrus.DebugLevel) + case "info": + Log.SetLevel(logrus.InfoLevel) + case "warn": + Log.SetLevel(logrus.WarnLevel) + case "error": + Log.SetLevel(logrus.ErrorLevel) + default: + Log.SetLevel(logrus.InfoLevel) + } +} + +// 便捷方法 +func Debug(args ...interface{}) { Log.Debug(args...) } +func Info(args ...interface{}) { Log.Info(args...) } +func Warn(args ...interface{}) { Log.Warn(args...) } +func Error(args ...interface{}) { Log.Error(args...) } +func Fatal(args ...interface{}) { Log.Fatal(args...) } + +func Debugf(format string, args ...interface{}) { Log.Debugf(format, args...) } +func Infof(format string, args ...interface{}) { Log.Infof(format, args...) } +func Warnf(format string, args ...interface{}) { Log.Warnf(format, args...) } +func Errorf(format string, args ...interface{}) { Log.Errorf(format, args...) } +func Fatalf(format string, args ...interface{}) { Log.Fatalf(format, args...) } + +// WithField 带字段的日志 +func WithField(key string, value interface{}) *logrus.Entry { + return Log.WithField(key, value) +} + +// WithFields 带多个字段的日志 +func WithFields(fields logrus.Fields) *logrus.Entry { + return Log.WithFields(fields) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..b573768 --- /dev/null +++ b/internal/middleware/auth.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "baihu/internal/constant" + "baihu/internal/utils" + + "github.com/gin-gonic/gin" +) + +// AuthRequired 认证中间件 +func AuthRequired() gin.HandlerFunc { + return func(c *gin.Context) { + token, err := c.Cookie(constant.CookieName) + if err != nil || token == "" { + utils.Unauthorized(c, "请先登录") + c.Abort() + return + } + + // 验证 token + userID, username, err := utils.ParseToken(token) + if err != nil { + utils.Unauthorized(c, "登录已过期,请重新登录") + c.Abort() + return + } + + // 将用户信息存入上下文 + c.Set("userID", userID) + c.Set("username", username) + c.Next() + } +} + +// SetAuthCookie 设置认证 Cookie +func SetAuthCookie(c *gin.Context, token string) { + c.SetCookie(constant.CookieName, token, constant.CookieMaxAge, "/", "", false, true) +} + +// ClearAuthCookie 清除认证 Cookie +func ClearAuthCookie(c *gin.Context) { + c.SetCookie(constant.CookieName, "", -1, "/", "", false, true) +} diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go new file mode 100644 index 0000000..c09b7ec --- /dev/null +++ b/internal/middleware/logger.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "fmt" + "time" + + "baihu/internal/logger" + + "github.com/gin-gonic/gin" +) + +// GinLogger 返回使用 logrus 的 Gin 日志中间件 +func GinLogger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + + c.Next() + + latency := time.Since(start) + status := c.Writer.Status() + clientIP := c.ClientIP() + method := c.Request.Method + + if query != "" { + path = path + "?" + query + } + + msg := fmt.Sprintf("%3d | %13v | %15s | %-7s %s", + status, latency, clientIP, method, path) + + if status >= 500 { + logger.Error(msg) + } else if status >= 400 { + logger.Warn(msg) + } else { + logger.Info(msg) + } + } +} + +// GinRecovery 返回使用 logrus 的 Gin 恢复中间件 +func GinRecovery() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + logger.Errorf("Panic recovered: %v | path: %s", err, c.Request.URL.Path) + c.AbortWithStatus(500) + } + }() + c.Next() + } +} diff --git a/internal/models/env.go b/internal/models/env.go new file mode 100644 index 0000000..183008c --- /dev/null +++ b/internal/models/env.go @@ -0,0 +1,38 @@ +package models + +import ( + "baihu/internal/constant" + + "gorm.io/gorm" +) + +// EnvironmentVariable represents an environment variable +type EnvironmentVariable struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:255;not null"` + Value string `json:"value" gorm:"type:text"` + Remark string `json:"remark" gorm:"size:500"` + UserID uint `json:"user_id" gorm:"index"` + CreatedAt LocalTime `json:"created_at"` + UpdatedAt LocalTime `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (EnvironmentVariable) TableName() string { + return constant.TablePrefix + "envs" +} + +// Script represents a script file +type Script struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:255;not null"` + Content string `json:"content" gorm:"type:text"` + UserID uint `json:"user_id" gorm:"index"` + CreatedAt LocalTime `json:"created_at"` + UpdatedAt LocalTime `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (Script) TableName() string { + return constant.TablePrefix + "scripts" +} \ No newline at end of file diff --git a/internal/models/setting.go b/internal/models/setting.go new file mode 100644 index 0000000..1e958d0 --- /dev/null +++ b/internal/models/setting.go @@ -0,0 +1,17 @@ +package models + +import ( + "baihu/internal/constant" +) + +// Setting 系统设置 +type Setting struct { + ID uint `json:"id" gorm:"primaryKey"` + Section string `json:"section" gorm:"size:50;not null;index:idx_section_key"` + Key string `json:"key" gorm:"size:100;not null;index:idx_section_key"` + Value string `json:"value" gorm:"type:text"` +} + +func (Setting) TableName() string { + return constant.TablePrefix + "settings" +} diff --git a/internal/models/task.go b/internal/models/task.go new file mode 100644 index 0000000..36cf9f1 --- /dev/null +++ b/internal/models/task.go @@ -0,0 +1,42 @@ +package models + +import ( + "baihu/internal/constant" + + "gorm.io/gorm" +) + +// Task represents a scheduled task +type Task struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:255;not null"` + Command string `json:"command" gorm:"type:text;not null"` + Schedule string `json:"schedule" gorm:"size:100"` // cron expression + Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟 + Enabled bool `json:"enabled" gorm:"default:true"` + LastRun *LocalTime `json:"last_run"` + NextRun *LocalTime `json:"next_run"` + CreatedAt LocalTime `json:"created_at"` + UpdatedAt LocalTime `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (Task) TableName() string { + return constant.TablePrefix + "tasks" +} + +// TaskLog represents a log entry for task execution +type TaskLog struct { + ID uint `json:"id" gorm:"primaryKey"` + TaskID uint `json:"task_id" gorm:"index"` + Command string `json:"command" gorm:"type:text"` + Output string `json:"-" gorm:"type:longtext"` // gzip+base64 compressed + Status string `json:"status" gorm:"size:20"` // success, failed + Duration int64 `json:"duration"` // milliseconds + ExitCode int `json:"exit_code"` + CreatedAt LocalTime `json:"created_at"` +} + +func (TaskLog) TableName() string { + return constant.TablePrefix + "task_logs" +} \ No newline at end of file diff --git a/internal/models/time.go b/internal/models/time.go new file mode 100644 index 0000000..6195278 --- /dev/null +++ b/internal/models/time.go @@ -0,0 +1,70 @@ +package models + +import ( + "database/sql/driver" + "fmt" + "time" +) + +const TimeFormat = "2006-01-02 15:04:05" + +// LocalTime 自定义时间类型,JSON 序列化为 "年-月-日 时:分:秒" 格式 +type LocalTime time.Time + +func (t LocalTime) MarshalJSON() ([]byte, error) { + tt := time.Time(t) + if tt.IsZero() { + return []byte("null"), nil + } + return []byte(fmt.Sprintf(`"%s"`, tt.Format(TimeFormat))), nil +} + +func (t *LocalTime) UnmarshalJSON(data []byte) error { + if string(data) == "null" || string(data) == `""` { + return nil + } + // 去掉引号 + s := string(data) + if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { + s = s[1 : len(s)-1] + } + tt, err := time.ParseInLocation(TimeFormat, s, time.Local) + if err != nil { + // 尝试解析 ISO 格式 + tt, err = time.Parse(time.RFC3339, s) + if err != nil { + return err + } + } + *t = LocalTime(tt) + return nil +} + +func (t LocalTime) Value() (driver.Value, error) { + return time.Time(t), nil +} + +func (t *LocalTime) Scan(v interface{}) error { + if v == nil { + return nil + } + switch val := v.(type) { + case time.Time: + *t = LocalTime(val) + case string: + tt, err := time.ParseInLocation(TimeFormat, val, time.Local) + if err != nil { + return err + } + *t = LocalTime(tt) + } + return nil +} + +func (t LocalTime) Time() time.Time { + return time.Time(t) +} + +func Now() LocalTime { + return LocalTime(time.Now()) +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..081808c --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,23 @@ +package models + +import ( + "baihu/internal/constant" + + "gorm.io/gorm" +) + +// User represents a system user +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"size:100;uniqueIndex;not null"` + Password string `json:"-" gorm:"size:255;not null"` + Email string `json:"email" gorm:"size:255"` + Role string `json:"role" gorm:"size:20;default:user"` // admin, user + CreatedAt LocalTime `json:"created_at"` + UpdatedAt LocalTime `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (User) TableName() string { + return constant.TablePrefix + "users" +} \ No newline at end of file diff --git a/internal/router/register.go b/internal/router/register.go new file mode 100644 index 0000000..aacbb34 --- /dev/null +++ b/internal/router/register.go @@ -0,0 +1,48 @@ +package router + +import ( + "baihu/internal/constant" + "baihu/internal/controllers" + "baihu/internal/services" +) + +var cronService *services.CronService + +func RegisterControllers() *Controllers { + // Initialize services + taskService := services.NewTaskService() + userService := services.NewUserService() + envService := services.NewEnvService() + scriptService := services.NewScriptService() + executorService := services.NewExecutorService(taskService) + settingsService := services.NewSettingsService() + + // 执行系统初始化 + initService := services.NewInitService(settingsService, userService) + initService.Initialize() + + // Initialize cron service + cronService = services.NewCronService(taskService, executorService) + cronService.Start() + + // Initialize and return controllers + return &Controllers{ + Task: controllers.NewTaskController(taskService, cronService), + Auth: controllers.NewAuthController(userService), + Env: controllers.NewEnvController(envService), + Script: controllers.NewScriptController(scriptService), + Executor: controllers.NewExecutorController(executorService), + File: controllers.NewFileController(constant.ScriptsWorkDir), + Dashboard: controllers.NewDashboardController(cronService, executorService), + Log: controllers.NewLogController(), + Terminal: controllers.NewTerminalController(), + Settings: controllers.NewSettingsController(userService), + } +} + +// StopCron stops the cron service gracefully +func StopCron() { + if cronService != nil { + cronService.Stop() + } +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..e4dd0b1 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,176 @@ +package router + +import ( + "io/fs" + "net/http" + + "baihu/internal/controllers" + "baihu/internal/middleware" + "baihu/internal/static" + + "github.com/gin-gonic/gin" +) + +type Controllers struct { + Task *controllers.TaskController + Auth *controllers.AuthController + Env *controllers.EnvController + Script *controllers.ScriptController + Executor *controllers.ExecutorController + File *controllers.FileController + Dashboard *controllers.DashboardController + Log *controllers.LogController + Terminal *controllers.TerminalController + Settings *controllers.SettingsController +} + +func mustSubFS(fsys fs.FS, dir string) fs.FS { + sub, err := fs.Sub(fsys, dir) + if err != nil { + panic(err) + } + return sub +} + +// cacheControl 返回设置 Cache-Control header 的中间件 +func cacheControl(value string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Cache-Control", value) + c.Next() + } +} + +func Setup(c *Controllers) *gin.Engine { + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.Use(middleware.GinLogger(), middleware.GinRecovery()) + + // Serve embedded Vue SPA static files with cache headers + staticFS := static.GetFS() + assetsGroup := router.Group("/assets") + assetsGroup.Use(cacheControl("public, max-age=31536000, immutable")) // 1 year cache for hashed assets + assetsGroup.StaticFS("/", http.FS(mustSubFS(staticFS, "assets"))) + + // Serve logo.svg with short cache + router.GET("/logo.svg", func(ctx *gin.Context) { + data, err := static.ReadFile("logo.svg") + if err != nil { + ctx.Status(404) + return + } + ctx.Header("Cache-Control", "public, max-age=86400") // 1 day + ctx.Data(200, "image/svg+xml", data) + }) + + // SPA fallback - serve index.html (no cache for HTML) + router.NoRoute(func(ctx *gin.Context) { + data, err := static.ReadFile("index.html") + if err != nil { + ctx.String(500, "index.html not found") + return + } + ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate") + ctx.Data(200, "text/html; charset=utf-8", data) + }) + + // API routes + api := router.Group("/api") + { + // Health check (无需认证) + api.GET("/ping", func(ctx *gin.Context) { + ctx.JSON(200, gin.H{"message": "pong"}) + }) + + // Authentication routes (无需认证) + auth := api.Group("/auth") + { + auth.POST("/login", c.Auth.Login) + auth.POST("/logout", c.Auth.Logout) + auth.POST("/register", c.Auth.Register) + } + + // 需要认证的路由 + authorized := api.Group("") + authorized.Use(middleware.AuthRequired()) + { + // 获取当前用户 + authorized.GET("/auth/me", c.Auth.GetCurrentUser) + + // Dashboard stats + authorized.GET("/stats", c.Dashboard.GetStats) + + // Task routes + tasks := authorized.Group("/tasks") + { + tasks.POST("", c.Task.CreateTask) + tasks.GET("", c.Task.GetTasks) + tasks.GET("/:id", c.Task.GetTask) + tasks.PUT("/:id", c.Task.UpdateTask) + tasks.DELETE("/:id", c.Task.DeleteTask) + } + + // Task execution routes + execution := authorized.Group("/execute") + { + execution.POST("/task/:id", c.Executor.ExecuteTask) + execution.POST("/command", c.Executor.ExecuteCommand) + execution.GET("/results", c.Executor.GetLastResults) + } + + // Environment variable routes + env := authorized.Group("/env") + { + env.POST("", c.Env.CreateEnvVar) + env.GET("", c.Env.GetEnvVars) + env.GET("/:id", c.Env.GetEnvVar) + env.PUT("/:id", c.Env.UpdateEnvVar) + env.DELETE("/:id", c.Env.DeleteEnvVar) + } + + // Script routes + scripts := authorized.Group("/scripts") + { + scripts.POST("", c.Script.CreateScript) + scripts.GET("", c.Script.GetScripts) + scripts.GET("/:id", c.Script.GetScript) + scripts.PUT("/:id", c.Script.UpdateScript) + scripts.DELETE("/:id", c.Script.DeleteScript) + } + + // File routes + files := authorized.Group("/files") + { + files.GET("/tree", c.File.GetFileTree) + files.GET("/content", c.File.GetFileContent) + files.POST("/content", c.File.SaveFileContent) + files.POST("/create", c.File.CreateFile) + files.POST("/delete", c.File.DeleteFile) + files.POST("/rename", c.File.RenameFile) + files.POST("/upload", c.File.UploadArchive) + files.POST("/upload-files", c.File.UploadFiles) + } + + // Log routes + logs := authorized.Group("/logs") + { + logs.GET("", c.Log.GetLogs) + logs.GET("/:id", c.Log.GetLogDetail) + } + + // Terminal routes + authorized.GET("/terminal/ws", c.Terminal.HandleWebSocket) + authorized.POST("/terminal/exec", c.Terminal.ExecuteShellCommand) + + // Settings routes + settings := authorized.Group("/settings") + { + settings.POST("/password", c.Settings.ChangePassword) + settings.POST("/clean-logs", c.Settings.CleanLogs) + settings.GET("/site", c.Settings.GetSiteSettings) + settings.GET("/about", c.Settings.GetAbout) + } + } + } + + return router +} diff --git a/internal/services/config_service.go b/internal/services/config_service.go new file mode 100644 index 0000000..84619c7 --- /dev/null +++ b/internal/services/config_service.go @@ -0,0 +1,71 @@ +package services + +import ( + "baihu/internal/constant" + "encoding/json" + "os" +) + +type ServerConfig struct { + Port int `json:"port"` + Host string `json:"host"` + SiteName string `json:"site_name"` +} + +type DatabaseConfig struct { + Type string `json:"type"` + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + Password string `json:"password"` + DBName string `json:"dbname"` + Path string `json:"path"` + TablePrefix string `json:"table_prefix"` +} + +type SecurityConfig struct { + JWTSecret string `json:"jwt_secret"` + PasswordSalt string `json:"password_salt"` +} + +type TaskConfig struct { + DefaultTimeout int `json:"default_timeout"` + LogRetentionDays int `json:"log_retention_days"` +} + +type AppConfig struct { + Server ServerConfig `json:"server"` + Database DatabaseConfig `json:"database"` + Security SecurityConfig `json:"security"` + Task TaskConfig `json:"task"` +} + +var Config *AppConfig + +func LoadConfig(path string) (*AppConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + Config = &AppConfig{} + if err := json.Unmarshal(data, Config); err != nil { + return nil, err + } + + // 设置表前缀到 constant 包 + if Config.Database.TablePrefix != "" { + constant.TablePrefix = Config.Database.TablePrefix + } + + // 设置 JWT 密钥 + if Config.Security.JWTSecret != "" { + constant.JWTSecret = Config.Security.JWTSecret + } + + return Config, nil +} + +func GetConfig() *AppConfig { + return Config +} diff --git a/internal/services/cron_service.go b/internal/services/cron_service.go new file mode 100644 index 0000000..bf41bdb --- /dev/null +++ b/internal/services/cron_service.go @@ -0,0 +1,150 @@ +package services + +import ( + "sync" + "time" + + "baihu/internal/database" + "baihu/internal/logger" + "baihu/internal/models" + + "github.com/robfig/cron/v3" +) + +// CronService manages scheduled tasks using robfig/cron +type CronService struct { + cron *cron.Cron + taskService *TaskService + executorService *ExecutorService + entryMap map[uint]cron.EntryID // task ID -> cron entry ID + mu sync.RWMutex +} + +// NewCronService creates a new cron service +func NewCronService(taskService *TaskService, executorService *ExecutorService) *CronService { + // 使用秒级精度的 cron parser,支持 5 位和 6 位表达式 + c := cron.New(cron.WithParser(cron.NewParser( + cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor, + ))) + + return &CronService{ + cron: c, + taskService: taskService, + executorService: executorService, + entryMap: make(map[uint]cron.EntryID), + } +} + +// Start starts the cron service and loads all enabled tasks +func (cs *CronService) Start() { + cs.loadTasks() + cs.cron.Start() + logger.Info("Cron service started") +} + +// Stop stops the cron service +func (cs *CronService) Stop() { + ctx := cs.cron.Stop() + <-ctx.Done() + logger.Info("Cron service stopped") +} + +// loadTasks loads all enabled tasks from database +func (cs *CronService) loadTasks() { + tasks := cs.taskService.GetTasks() + for _, task := range tasks { + if task.Enabled { + err := cs.AddTask(&task) + if err != nil { + return + } + } + } +} + +// AddTask adds a task to the cron scheduler +func (cs *CronService) AddTask(task *models.Task) error { + cs.mu.Lock() + + // 如果已存在,先移除 + if entryID, exists := cs.entryMap[task.ID]; exists { + cs.cron.Remove(entryID) + delete(cs.entryMap, task.ID) + } + + taskID := task.ID + entryID, err := cs.cron.AddFunc(task.Schedule, func() { + cs.runTask(taskID) + }) + if err != nil { + cs.mu.Unlock() + logger.Errorf("Failed to add task %d: %v", task.ID, err) + return err + } + + cs.entryMap[task.ID] = entryID + cs.mu.Unlock() + + logger.Infof("Task %d (%s) scheduled with cron: %s", task.ID, task.Name, task.Schedule) + + // 更新下次运行时间 + cs.updateNextRun(task.ID) + return nil +} + +// RemoveTask removes a task from the cron scheduler +func (cs *CronService) RemoveTask(taskID uint) { + cs.mu.Lock() + defer cs.mu.Unlock() + + if entryID, exists := cs.entryMap[taskID]; exists { + cs.cron.Remove(entryID) + delete(cs.entryMap, taskID) + logger.Infof("Task %d removed from scheduler", taskID) + } +} + +// runTask executes a task and updates its status +func (cs *CronService) runTask(taskID uint) { + logger.Infof("Running task %d", taskID) + + // 更新 last_run + now := time.Now() + database.DB.Model(&models.Task{}).Where("id = ?", taskID).Update("last_run", now) + + // 执行任务 + cs.executorService.ExecuteTask(int(taskID)) + + // 更新 next_run + cs.updateNextRun(taskID) +} + +// updateNextRun updates the next run time for a task +func (cs *CronService) updateNextRun(taskID uint) { + cs.mu.RLock() + entryID, exists := cs.entryMap[taskID] + cs.mu.RUnlock() + + if !exists { + return + } + + entry := cs.cron.Entry(entryID) + if !entry.Next.IsZero() { + database.DB.Model(&models.Task{}).Where("id = ?", taskID).Update("next_run", entry.Next) + } +} + +// ValidateCron validates a cron expression +func (cs *CronService) ValidateCron(expression string) error { + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) + _, err := parser.Parse(expression) + return err +} + +// GetScheduledCount returns the number of scheduled tasks +func (cs *CronService) GetScheduledCount() int { + cs.mu.RLock() + defer cs.mu.RUnlock() + return len(cs.entryMap) +} diff --git a/internal/services/env_service.go b/internal/services/env_service.go new file mode 100644 index 0000000..c8305fa --- /dev/null +++ b/internal/services/env_service.go @@ -0,0 +1,68 @@ +package services + +import ( + "baihu/internal/database" + "baihu/internal/models" +) + +type EnvService struct{} + +func NewEnvService() *EnvService { + return &EnvService{} +} + +func (es *EnvService) CreateEnvVar(name, value, remark string, userID int) *models.EnvironmentVariable { + env := &models.EnvironmentVariable{ + Name: name, + Value: value, + Remark: remark, + UserID: uint(userID), + } + database.DB.Create(env) + return env +} + +func (es *EnvService) GetEnvVarsByUserID(userID int) []models.EnvironmentVariable { + var envs []models.EnvironmentVariable + database.DB.Where("user_id = ?", userID).Find(&envs) + return envs +} + +func (es *EnvService) GetEnvVarsWithPagination(userID int, name string, page, pageSize int) ([]models.EnvironmentVariable, int64) { + var envs []models.EnvironmentVariable + var total int64 + + query := database.DB.Model(&models.EnvironmentVariable{}).Where("user_id = ?", userID) + if name != "" { + query = query.Where("name LIKE ?", "%"+name+"%") + } + + query.Count(&total) + query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&envs) + return envs, total +} + +func (es *EnvService) GetEnvVarByID(id int) *models.EnvironmentVariable { + var env models.EnvironmentVariable + if err := database.DB.First(&env, id).Error; err != nil { + return nil + } + return &env +} + +func (es *EnvService) UpdateEnvVar(id int, name, value, remark string) *models.EnvironmentVariable { + var env models.EnvironmentVariable + if err := database.DB.First(&env, id).Error; err != nil { + return nil + } + env.Name = name + env.Value = value + env.Remark = remark + database.DB.Save(&env) + return &env +} + +func (es *EnvService) DeleteEnvVar(id int) bool { + result := database.DB.Delete(&models.EnvironmentVariable{}, id) + return result.RowsAffected > 0 +} diff --git a/internal/services/executor_service.go b/internal/services/executor_service.go new file mode 100644 index 0000000..48564e5 --- /dev/null +++ b/internal/services/executor_service.go @@ -0,0 +1,183 @@ +package services + +import ( + "bytes" + "context" + "os/exec" + "sync" + "time" + + "baihu/internal/constant" + "baihu/internal/database" + "baihu/internal/logger" + "baihu/internal/models" + "baihu/internal/utils" +) + +// ExecutionResult represents the result of a task execution +type ExecutionResult struct { + TaskID int + Success bool + Output string + Error string + Start time.Time + End time.Time +} + +// ExecutorService handles task execution +type ExecutorService struct { + taskService *TaskService + results []ExecutionResult + runningTasks map[int]bool // 正在运行的任务 + mu sync.RWMutex +} + +// NewExecutorService creates a new executor service +func NewExecutorService(taskService *TaskService) *ExecutorService { + return &ExecutorService{ + taskService: taskService, + results: make([]ExecutionResult, 0), + runningTasks: make(map[int]bool), + } +} + +// ExecuteTask executes a task by ID +func (es *ExecutorService) ExecuteTask(taskID int) *ExecutionResult { + task := es.taskService.GetTaskByID(taskID) + if task == nil { + return &ExecutionResult{ + TaskID: taskID, + Success: false, + Error: "Task not found", + Start: time.Now(), + End: time.Now(), + } + } + + // 标记任务开始运行 + es.mu.Lock() + es.runningTasks[taskID] = true + es.mu.Unlock() + + // 使用任务配置的超时时间 + timeout := task.Timeout + if timeout <= 0 { + timeout = constant.DefaultTaskTimeout + } + result := es.ExecuteCommandWithTimeout(task.Command, time.Duration(timeout)*time.Minute) + result.TaskID = taskID + + // 标记任务结束 + es.mu.Lock() + delete(es.runningTasks, taskID) + es.mu.Unlock() + + // Save log to database + es.saveTaskLog(uint(taskID), task.Command, result) + + return result +} + +// GetRunningCount 获取正在运行的任务数量 +func (es *ExecutorService) GetRunningCount() int { + es.mu.RLock() + defer es.mu.RUnlock() + return len(es.runningTasks) +} + +// ExecuteCommand executes a shell command with default timeout +func (es *ExecutorService) ExecuteCommand(command string) *ExecutionResult { + return es.ExecuteCommandWithTimeout(command, time.Duration(constant.DefaultTaskTimeout)*time.Minute) +} + +// ExecuteCommandWithTimeout executes a shell command with specified timeout +func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout time.Duration) *ExecutionResult { + result := &ExecutionResult{ + Success: false, + Start: time.Now(), + } + + // Create a context with timeout + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Execute the command + shell, args := utils.GetShellCommand(command) + cmd := exec.CommandContext(ctx, shell, args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + result.End = time.Now() + + // Process results + result.Output = stdout.String() + if err != nil { + if ctx.Err() == context.DeadlineExceeded { + result.Error = "执行超时\n" + stderr.String() + } else { + result.Error = err.Error() + "\n" + stderr.String() + } + } else { + result.Success = true + } + + // Store result + es.mu.Lock() + es.results = append(es.results, *result) + // Keep only the last 100 results to prevent memory issues + if len(es.results) > 100 { + es.results = es.results[1:] + } + es.mu.Unlock() + + return result +} + +// GetLastResults returns the last execution results +func (es *ExecutorService) GetLastResults(count int) []ExecutionResult { + es.mu.RLock() + defer es.mu.RUnlock() + + start := 0 + if len(es.results) > count { + start = len(es.results) - count + } + + results := make([]ExecutionResult, len(es.results[start:])) + copy(results, es.results[start:]) + return results +} + +// saveTaskLog saves execution log to database with gzip+base64 compression +func (es *ExecutorService) saveTaskLog(taskID uint, command string, result *ExecutionResult) { + output := result.Output + if result.Error != "" { + output += "\n[ERROR]\n" + result.Error + } + + // Compress output + compressed, err := utils.CompressToBase64(output) + if err != nil { + logger.Errorf("Failed to compress log: %v", err) + compressed = "" + } + + status := "success" + if !result.Success { + status = "failed" + } + + taskLog := &models.TaskLog{ + TaskID: taskID, + Command: command, + Output: compressed, + Status: status, + Duration: result.End.Sub(result.Start).Milliseconds(), + } + + if err := database.DB.Create(taskLog).Error; err != nil { + logger.Errorf("Failed to save task log: %v", err) + } +} diff --git a/internal/services/init_service.go b/internal/services/init_service.go new file mode 100644 index 0000000..7e1a9ae --- /dev/null +++ b/internal/services/init_service.go @@ -0,0 +1,57 @@ +package services + +import ( + "baihu/internal/logger" +) + +const ( + InitSection = "system" + InitKey = "initialized" + InitValue = "true" +) + +type InitService struct { + settingsService *SettingsService + userService *UserService +} + +func NewInitService(settingsService *SettingsService, userService *UserService) *InitService { + return &InitService{ + settingsService: settingsService, + userService: userService, + } +} + +// Initialize 执行初始化,如果已初始化则跳过 +func (s *InitService) Initialize() { + if s.IsInitialized() { + logger.Info("系统已初始化,跳过") + return + } + + logger.Info("开始初始化系统...") + + // 创建管理员账号 + s.createAdminUser() + + // 标记为已初始化 + s.settingsService.Set(InitSection, InitKey, InitValue) + logger.Info("系统初始化完成") +} + +// IsInitialized 检查是否已初始化 +func (s *InitService) IsInitialized() bool { + return s.settingsService.Get(InitSection, InitKey) == InitValue +} + +// createAdminUser 创建管理员账号 +func (s *InitService) createAdminUser() { + existingUser := s.userService.GetUserByUsername("admin") + if existingUser != nil { + logger.Info("管理员账号已存在,跳过创建") + return + } + + s.userService.CreateUser("admin", "123456", "admin@local", "admin") + logger.Info("管理员账号创建成功: admin / 123456") +} diff --git a/internal/services/script_service.go b/internal/services/script_service.go new file mode 100644 index 0000000..4f317aa --- /dev/null +++ b/internal/services/script_service.go @@ -0,0 +1,52 @@ +package services + +import ( + "baihu/internal/database" + "baihu/internal/models" +) + +type ScriptService struct{} + +func NewScriptService() *ScriptService { + return &ScriptService{} +} + +func (ss *ScriptService) CreateScript(name, content string, userID int) *models.Script { + script := &models.Script{ + Name: name, + Content: content, + UserID: uint(userID), + } + database.DB.Create(script) + return script +} + +func (ss *ScriptService) GetScriptsByUserID(userID int) []models.Script { + var scripts []models.Script + database.DB.Where("user_id = ?", userID).Find(&scripts) + return scripts +} + +func (ss *ScriptService) GetScriptByID(id int) *models.Script { + var script models.Script + if err := database.DB.First(&script, id).Error; err != nil { + return nil + } + return &script +} + +func (ss *ScriptService) UpdateScript(id int, name, content string) *models.Script { + var script models.Script + if err := database.DB.First(&script, id).Error; err != nil { + return nil + } + script.Name = name + script.Content = content + database.DB.Save(&script) + return &script +} + +func (ss *ScriptService) DeleteScript(id int) bool { + result := database.DB.Delete(&models.Script{}, id) + return result.RowsAffected > 0 +} diff --git a/internal/services/settings_service.go b/internal/services/settings_service.go new file mode 100644 index 0000000..843452d --- /dev/null +++ b/internal/services/settings_service.go @@ -0,0 +1,69 @@ +package services + +import ( + "baihu/internal/database" + "baihu/internal/models" +) + +type SettingsService struct{} + +func NewSettingsService() *SettingsService { + return &SettingsService{} +} + +// Get 获取设置值 +func (s *SettingsService) Get(section, key string) string { + var setting models.Setting + if err := database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error; err != nil { + return "" + } + return setting.Value +} + +// GetWithDefault 获取设置值,如果不存在则返回默认值 +func (s *SettingsService) GetWithDefault(section, key, defaultValue string) string { + value := s.Get(section, key) + if value == "" { + return defaultValue + } + return value +} + +// Set 设置值 +func (s *SettingsService) Set(section, key, value string) error { + var setting models.Setting + result := database.DB.Where("section = ? AND key = ?", section, key).First(&setting) + if result.Error != nil { + // 不存在则创建 + setting = models.Setting{ + Section: section, + Key: key, + Value: value, + } + return database.DB.Create(&setting).Error + } + // 存在则更新 + return database.DB.Model(&setting).Update("value", value).Error +} + +// GetBySection 获取某个 section 下的所有设置 +func (s *SettingsService) GetBySection(section string) map[string]string { + var settings []models.Setting + database.DB.Where("section = ?", section).Find(&settings) + + result := make(map[string]string) + for _, s := range settings { + result[s.Key] = s.Value + } + return result +} + +// Delete 删除设置 +func (s *SettingsService) Delete(section, key string) error { + return database.DB.Where("section = ? AND key = ?", section, key).Delete(&models.Setting{}).Error +} + +// DeleteBySection 删除某个 section 下的所有设置 +func (s *SettingsService) DeleteBySection(section string) error { + return database.DB.Where("section = ?", section).Delete(&models.Setting{}).Error +} diff --git a/internal/services/task_service.go b/internal/services/task_service.go new file mode 100644 index 0000000..70e155c --- /dev/null +++ b/internal/services/task_service.go @@ -0,0 +1,71 @@ +package services + +import ( + "baihu/internal/database" + "baihu/internal/models" +) + +type TaskService struct{} + +func NewTaskService() *TaskService { + return &TaskService{} +} + +func (ts *TaskService) CreateTask(name, command, schedule string) *models.Task { + task := &models.Task{ + Name: name, + Command: command, + Schedule: schedule, + Enabled: true, + } + database.DB.Create(task) + return task +} + +func (ts *TaskService) GetTasks() []models.Task { + var tasks []models.Task + database.DB.Find(&tasks) + return tasks +} + +// GetTasksWithPagination 分页获取任务列表 +func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string) ([]models.Task, int64) { + var tasks []models.Task + var total int64 + + query := database.DB.Model(&models.Task{}) + if name != "" { + query = query.Where("name LIKE ?", "%"+name+"%") + } + + query.Count(&total) + query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks) + + return tasks, total +} + +func (ts *TaskService) GetTaskByID(id int) *models.Task { + var task models.Task + if err := database.DB.First(&task, id).Error; err != nil { + return nil + } + return &task +} + +func (ts *TaskService) UpdateTask(id int, name, command, schedule string, enabled bool) *models.Task { + var task models.Task + if err := database.DB.First(&task, id).Error; err != nil { + return nil + } + task.Name = name + task.Command = command + task.Schedule = schedule + task.Enabled = enabled + database.DB.Save(&task) + return &task +} + +func (ts *TaskService) DeleteTask(id int) bool { + result := database.DB.Delete(&models.Task{}, id) + return result.RowsAffected > 0 +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go new file mode 100644 index 0000000..0e9932e --- /dev/null +++ b/internal/services/user_service.go @@ -0,0 +1,67 @@ +package services + +import ( + "crypto/sha256" + "encoding/hex" + + "baihu/internal/database" + "baihu/internal/models" +) + +type UserService struct{} + +func NewUserService() *UserService { + return &UserService{} +} + +func (us *UserService) hashPassword(password string) string { + salt := "" + if Config != nil { + salt = Config.Security.PasswordSalt + } + hash := sha256.Sum256([]byte(password + salt)) + return hex.EncodeToString(hash[:]) +} + +func (us *UserService) CreateUser(username, password, email, role string) *models.User { + user := &models.User{ + Username: username, + Password: us.hashPassword(password), + Email: email, + Role: role, + } + database.DB.Create(user) + return user +} + +func (us *UserService) GetUserByUsername(username string) *models.User { + var user models.User + if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil { + return nil + } + return &user +} + +func (us *UserService) ValidatePassword(user *models.User, password string) bool { + return user.Password == us.hashPassword(password) +} + +func (us *UserService) EnsureAdminExists() { + var count int64 + database.DB.Model(&models.User{}).Where("role = ?", "admin").Count(&count) + if count == 0 { + us.CreateUser("admin", "admin123", "admin@local", "admin") + } +} + +func (us *UserService) AuthenticateUser(username, password string) bool { + user := us.GetUserByUsername(username) + if user == nil { + return false + } + return us.ValidatePassword(user, password) +} + +func (us *UserService) UpdatePassword(userID uint, newPassword string) error { + return database.DB.Model(&models.User{}).Where("id = ?", userID).Update("password", us.hashPassword(newPassword)).Error +} diff --git a/internal/static/embed.go b/internal/static/embed.go new file mode 100644 index 0000000..fe3529a --- /dev/null +++ b/internal/static/embed.go @@ -0,0 +1,33 @@ +package static + +import ( + "embed" + "io/fs" + "net/http" +) + +//go:embed dist/* +var distFS embed.FS + +// GetFileSystem 返回嵌入的静态文件系统 +func GetFileSystem() http.FileSystem { + subFS, err := fs.Sub(distFS, "dist") + if err != nil { + panic(err) + } + return http.FS(subFS) +} + +// GetFS 返回嵌入的 fs.FS +func GetFS() fs.FS { + subFS, err := fs.Sub(distFS, "dist") + if err != nil { + panic(err) + } + return subFS +} + +// ReadFile 读取嵌入的文件 +func ReadFile(name string) ([]byte, error) { + return distFS.ReadFile("dist/" + name) +} diff --git a/internal/utils/archive.go b/internal/utils/archive.go new file mode 100644 index 0000000..d83496d --- /dev/null +++ b/internal/utils/archive.go @@ -0,0 +1,125 @@ +package utils + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "io" + "os" + "path/filepath" + "strings" +) + +func ExtractZip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + + // 安全检查:防止路径遍历 + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + continue + } + + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, 0755) + continue + } + + if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { + return err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + outFile.Close() + return err + } + + _, err = io.Copy(outFile, rc) + outFile.Close() + rc.Close() + + if err != nil { + return err + } + } + return nil +} + +func ExtractTar(src, dest string) error { + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + return extractTarReader(tar.NewReader(file), dest) +} + +func ExtractTarGz(src, dest string) error { + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + gzr, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gzr.Close() + + return extractTarReader(tar.NewReader(gzr), dest) +} + +func extractTarReader(tr *tar.Reader, dest string) error { + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + fpath := filepath.Join(dest, header.Name) + + // 安全检查:防止路径遍历 + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + continue + } + + switch header.Typeflag { + case tar.TypeDir: + os.MkdirAll(fpath, 0755) + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { + return err + } + + outFile, err := os.Create(fpath) + if err != nil { + return err + } + + if _, err := io.Copy(outFile, tr); err != nil { + outFile.Close() + return err + } + outFile.Close() + + os.Chmod(fpath, os.FileMode(header.Mode)) + } + } + return nil +} diff --git a/internal/utils/compress.go b/internal/utils/compress.go new file mode 100644 index 0000000..5761df8 --- /dev/null +++ b/internal/utils/compress.go @@ -0,0 +1,39 @@ +package utils + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "io" +) + +// CompressToBase64 compresses data using gzip and encodes to base64 +func CompressToBase64(data string) (string, error) { + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write([]byte(data)); err != nil { + return "", err + } + if err := gz.Close(); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil +} + +// DecompressFromBase64 decodes base64 and decompresses gzip data +func DecompressFromBase64(data string) (string, error) { + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return "", err + } + gz, err := gzip.NewReader(bytes.NewReader(decoded)) + if err != nil { + return "", err + } + defer gz.Close() + result, err := io.ReadAll(gz) + if err != nil { + return "", err + } + return string(result), nil +} diff --git a/internal/utils/pagination.go b/internal/utils/pagination.go new file mode 100644 index 0000000..0df0995 --- /dev/null +++ b/internal/utils/pagination.go @@ -0,0 +1,45 @@ +package utils + +import ( + "strconv" + + "baihu/internal/constant" + + "github.com/gin-gonic/gin" +) + +// Pagination 分页参数 +type Pagination struct { + Page int + PageSize int +} + +// ParsePagination 从请求中解析分页参数 +func ParsePagination(c *gin.Context) Pagination { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", strconv.Itoa(constant.DefaultPageSize))) + + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = constant.DefaultPageSize + } + + return Pagination{Page: page, PageSize: pageSize} +} + +// Offset 计算偏移量 +func (p Pagination) Offset() int { + return (p.Page - 1) * p.PageSize +} + +// PaginatedResponse 分页响应 +func PaginatedResponse(c *gin.Context, data interface{}, total int64, p Pagination) { + Success(c, gin.H{ + "data": data, + "total": total, + "page": p.Page, + "page_size": p.PageSize, + }) +} diff --git a/internal/utils/response.go b/internal/utils/response.go new file mode 100644 index 0000000..ae9e773 --- /dev/null +++ b/internal/utils/response.go @@ -0,0 +1,55 @@ +package utils + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} + +func Success(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: 200, + Msg: "success", + Data: data, + }) +} + +func SuccessMsg(c *gin.Context, msg string) { + c.JSON(http.StatusOK, Response{ + Code: 200, + Msg: msg, + }) +} + +func Error(c *gin.Context, code int, msg string) { + c.JSON(http.StatusOK, Response{ + Code: code, + Msg: msg, + }) +} + +func BadRequest(c *gin.Context, msg string) { + Error(c, 400, msg) +} + +func Unauthorized(c *gin.Context, msg string) { + Error(c, 401, msg) +} + +func Forbidden(c *gin.Context, msg string) { + Error(c, 403, msg) +} + +func NotFound(c *gin.Context, msg string) { + Error(c, 404, msg) +} + +func ServerError(c *gin.Context, msg string) { + Error(c, 500, msg) +} diff --git a/internal/utils/shell.go b/internal/utils/shell.go new file mode 100644 index 0000000..9e68380 --- /dev/null +++ b/internal/utils/shell.go @@ -0,0 +1,54 @@ +package utils + +import ( + "os" + "os/exec" + "runtime" +) + +// GetShell 返回当前操作系统的 shell 和参数 +func GetShell() (shell string, args []string) { + if runtime.GOOS == "windows" { + return "cmd", []string{} + } + + // 优先使用环境变量中的 SHELL + if envShell := os.Getenv("SHELL"); envShell != "" { + return envShell, []string{} + } + + // macOS 默认使用 zsh + if runtime.GOOS == "darwin" { + if _, err := exec.LookPath("/bin/zsh"); err == nil { + return "/bin/zsh", []string{} + } + } + + // Linux 默认使用 bash + return "/bin/bash", []string{} +} + +// GetShellCommand 返回执行命令的 shell 和参数 +func GetShellCommand(command string) (shell string, args []string) { + shell, _ = GetShell() + if runtime.GOOS == "windows" { + return shell, []string{"/c", command} + } + return shell, []string{"-c", command} +} + +// NewShellCmd 创建一个交互式 shell 命令 +func NewShellCmd() *exec.Cmd { + shell, _ := GetShell() + if runtime.GOOS == "windows" { + return exec.Command(shell) + } + // Unix 系统使用 -i 启用交互模式 + return exec.Command(shell, "-i") +} + +// NewShellCommandCmd 创建一个执行指定命令的 shell 命令 +func NewShellCommandCmd(command string) *exec.Cmd { + shell, args := GetShellCommand(command) + return exec.Command(shell, args...) +} diff --git a/internal/utils/token.go b/internal/utils/token.go new file mode 100644 index 0000000..166f889 --- /dev/null +++ b/internal/utils/token.go @@ -0,0 +1,48 @@ +package utils + +import ( + "errors" + "time" + + "baihu/internal/constant" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +// GenerateToken 生成 JWT token +func GenerateToken(userID uint, username string) (string, error) { + claims := Claims{ + UserID: userID, + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(constant.TokenExpireDays) * 24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(constant.JWTSecret)) +} + +// ParseToken 解析 JWT token +func ParseToken(tokenString string) (uint, string, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { + return []byte(constant.JWTSecret), nil + }) + + if err != nil { + return 0, "", err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims.UserID, claims.Username, nil + } + + return 0, "", errors.New("invalid token") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4472c9c --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "baihu/internal/bootstrap" + +func main() { + bootstrap.New().Run() +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..7c1f03f --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +# logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/web/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..27d5bb4 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,2790 @@ +{ + "name": "web-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-ui", + "version": "0.0.0", + "dependencies": { + "@guolao/vue-monaco-editor": "^1.6.0", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/vue-table": "^8.21.3", + "@vueuse/core": "^14.1.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "jinrishici": "^1.0.6", + "lucide-react": "^0.562.0", + "lucide-vue-next": "^0.562.0", + "pako": "^2.1.0", + "radix-vue": "^1.9.17", + "reka-ui": "^2.6.1", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "vaul-vue": "^0.4.1", + "vue": "^3.5.24", + "vue-router": "^4.6.4", + "vue-sonner": "^2.0.9", + "xterm": "^5.3.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pako": "^2.0.4", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vue-tsc": "^3.1.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@floating-ui/vue": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.9.tgz", + "integrity": "sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4", + "@floating-ui/utils": "^0.2.10", + "vue-demi": ">=0.13.0" + } + }, + "node_modules/@floating-ui/vue/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@guolao/vue-monaco-editor": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@guolao/vue-monaco-editor/-/vue-monaco-editor-1.6.0.tgz", + "integrity": "sha512-w2IiJ6eJGGeuIgCK6EKZOAfhHTTUB5aZwslzwGbZ5e89Hb4avx6++GkLTW8p84Sng/arFMjLPPxSBI56cFudyQ==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.6.1", + "vue-demi": "latest" + }, + "peerDependencies": { + "@vue/composition-api": "^1.7.2", + "monaco-editor": ">=0.43.0", + "vue": "^2.6.14 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@guolao/vue-monaco-editor/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@internationalized/date": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz", + "integrity": "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", + "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.13", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.13.tgz", + "integrity": "sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/vue-table/-/vue-table-8.21.3.tgz", + "integrity": "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": ">=3.2" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.13", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.13.tgz", + "integrity": "sha512-Cf2xIEE8nWAfsX0N5nihkPYMeQRT+pHt4NEkuP8rNCn6lVnLDiV8rC8IeIxbKmQC0yPnj4SIBLwXYVf86xxKTQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.26.tgz", + "integrity": "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.26" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.26.tgz", + "integrity": "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.26", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.26.tgz", + "integrity": "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.26", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.8.tgz", + "integrity": "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.26", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", + "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.1.0", + "@vueuse/shared": "14.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", + "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", + "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "node_modules/alien-signals": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.1.tgz", + "integrity": "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jinrishici": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/jinrishici/-/jinrishici-1.0.6.tgz", + "integrity": "sha512-4Bk5VHo6xg9DspGS8y8I7D6kftA6DYy4rSvwpdtxFvh9DidXMgf+NSZKAXrcLTi8S5NjqmwkpAKYPHXRURH64Q==", + "license": "Apache-2.0" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lucide-vue-next": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.562.0.tgz", + "integrity": "sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/radix-vue": { + "version": "1.9.17", + "resolved": "https://registry.npmjs.org/radix-vue/-/radix-vue-1.9.17.tgz", + "integrity": "sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.7", + "@floating-ui/vue": "^1.1.0", + "@internationalized/date": "^3.5.4", + "@internationalized/number": "^3.5.3", + "@tanstack/vue-virtual": "^3.8.1", + "@vueuse/core": "^10.11.0", + "@vueuse/shared": "^10.11.0", + "aria-hidden": "^1.2.4", + "defu": "^6.1.4", + "fast-deep-equal": "^3.1.3", + "nanoid": "^5.0.7" + }, + "peerDependencies": { + "vue": ">= 3.2.0" + } + }, + "node_modules/radix-vue/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/radix-vue/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/radix-vue/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/radix-vue/node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/radix-vue/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reka-ui": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.6.1.tgz", + "integrity": "sha512-XK7cJDQoNuGXfCNzBBo/81Yg/OgjPwvbabnlzXG2VsdSgNsT6iIkuPBPr+C0Shs+3bb0x0lbPvgQAhMSCKm5Ww==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.13", + "@floating-ui/vue": "^1.1.6", + "@internationalized/date": "^3.5.0", + "@internationalized/number": "^3.5.0", + "@tanstack/vue-virtual": "^3.12.0", + "@vueuse/core": "^12.5.0", + "@vueuse/shared": "^12.5.0", + "aria-hidden": "^1.2.4", + "defu": "^6.1.4", + "ohash": "^2.0.11" + }, + "peerDependencies": { + "vue": ">= 3.2.0" + } + }, + "node_modules/reka-ui/node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/reka-ui/node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/reka-ui/node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/rollup": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vaul-vue": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/vaul-vue/-/vaul-vue-0.4.1.tgz", + "integrity": "sha512-A6jOWOZX5yvyo1qMn7IveoWN91mJI5L3BUKsIwkg6qrTGgHs1Sb1JF/vyLJgnbN1rH4OOOxFbtqL9A46bOyGUQ==", + "dependencies": { + "@vueuse/core": "^10.8.0", + "reka-ui": "^2.0.0", + "vue": "^3.4.5" + }, + "peerDependencies": { + "reka-ui": "^2.0.0", + "vue": "^3.3.0" + } + }, + "node_modules/vaul-vue/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/vaul-vue/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vaul-vue/node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vaul-vue/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vaul-vue/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vaul-vue/node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-sonner": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-2.0.9.tgz", + "integrity": "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==", + "license": "MIT", + "peerDependencies": { + "@nuxt/kit": "^4.0.3", + "@nuxt/schema": "^4.0.3", + "nuxt": "^4.0.3" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@nuxt/schema": { + "optional": true + }, + "nuxt": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.8.tgz", + "integrity": "sha512-deKgwx6exIHeZwF601P1ktZKNF0bepaSN4jBU3AsbldPx9gylUc1JDxYppl82yxgkAgaz0Y0LCLOi+cXe9HMYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.26", + "@vue/language-core": "3.1.8" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "license": "MIT" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..1bd2f0c --- /dev/null +++ b/web/package.json @@ -0,0 +1,44 @@ +{ + "name": "web-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@guolao/vue-monaco-editor": "^1.6.0", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/vue-table": "^8.21.3", + "@vueuse/core": "^14.1.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "jinrishici": "^1.0.6", + "lucide-react": "^0.562.0", + "lucide-vue-next": "^0.562.0", + "pako": "^2.1.0", + "radix-vue": "^1.9.17", + "reka-ui": "^2.6.1", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "vaul-vue": "^0.4.1", + "vue": "^3.5.24", + "vue-router": "^4.6.4", + "vue-sonner": "^2.0.9", + "xterm": "^5.3.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pako": "^2.0.4", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "vite": "^7.2.4", + "vue-tsc": "^3.1.4" + } +} diff --git a/web/public/logo.svg b/web/public/logo.svg new file mode 100644 index 0000000..e9dac84 --- /dev/null +++ b/web/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..84afb8b --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,9 @@ + + + diff --git a/web/src/api/index.ts b/web/src/api/index.ts new file mode 100644 index 0000000..39005f5 --- /dev/null +++ b/web/src/api/index.ts @@ -0,0 +1,254 @@ +const BASE_URL = '/api' + +interface ApiResponse { + code: number + msg: string + data: T +} + +async function request(url: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${url}`, { + ...options, + credentials: 'include', // 携带 Cookie + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }) + + const json: ApiResponse = await res.json() + + if (json.code === 401) { + // 未登录或登录过期,跳转到登录页 + window.location.href = '/login' + throw new Error(json.msg || '请先登录') + } + + if (json.code !== 200) { + throw new Error(json.msg || '请求失败') + } + + return json.data +} + +// 检查登录状态(不触发自动跳转) +export async function checkAuth(): Promise { + try { + const res = await fetch(`${BASE_URL}/auth/me`, { + credentials: 'include', + headers: { 'Content-Type': 'application/json' } + }) + const json: ApiResponse<{ username: string }> = await res.json() + return json.code === 200 + } catch { + return false + } +} + +export const api = { + auth: { + login: (data: { username: string; password: string }) => + request<{ user: string }>('/auth/login', { method: 'POST', body: JSON.stringify(data) }), + logout: () => request('/auth/logout', { method: 'POST' }), + me: () => request<{ username: string }>('/auth/me'), + register: (data: { username: string; password: string; email: string }) => + request('/auth/register', { method: 'POST', body: JSON.stringify(data) }) + }, + tasks: { + list: (params?: { page?: number; page_size?: number; name?: string }) => { + const query = new URLSearchParams() + if (params?.page) query.set('page', String(params.page)) + if (params?.page_size) query.set('page_size', String(params.page_size)) + if (params?.name) query.set('name', params.name) + return request(`/tasks?${query}`) + }, + create: (data: Partial) => request('/tasks', { method: 'POST', body: JSON.stringify(data) }), + update: (id: number, data: Partial) => request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + delete: (id: number) => request(`/tasks/${id}`, { method: 'DELETE' }), + execute: (id: number) => request(`/execute/task/${id}`, { method: 'POST' }) + }, + scripts: { + list: () => request('/scripts'), + create: (data: Partial + + diff --git a/web/src/components/Pagination.vue b/web/src/components/Pagination.vue new file mode 100644 index 0000000..a44f8c1 --- /dev/null +++ b/web/src/components/Pagination.vue @@ -0,0 +1,44 @@ + + + diff --git a/web/src/components/ThemeToggle.vue b/web/src/components/ThemeToggle.vue new file mode 100644 index 0000000..b3d7c9c --- /dev/null +++ b/web/src/components/ThemeToggle.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialog.vue b/web/src/components/ui/alert-dialog/AlertDialog.vue new file mode 100644 index 0000000..b6e6b4b --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialog.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogAction.vue b/web/src/components/ui/alert-dialog/AlertDialogAction.vue new file mode 100644 index 0000000..09cf6fc --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogAction.vue @@ -0,0 +1,18 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogCancel.vue b/web/src/components/ui/alert-dialog/AlertDialogCancel.vue new file mode 100644 index 0000000..e261894 --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogCancel.vue @@ -0,0 +1,25 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogContent.vue b/web/src/components/ui/alert-dialog/AlertDialogContent.vue new file mode 100644 index 0000000..4597f0d --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogContent.vue @@ -0,0 +1,44 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogDescription.vue b/web/src/components/ui/alert-dialog/AlertDialogDescription.vue new file mode 100644 index 0000000..69642c9 --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogDescription.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogFooter.vue b/web/src/components/ui/alert-dialog/AlertDialogFooter.vue new file mode 100644 index 0000000..50d4098 --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogFooter.vue @@ -0,0 +1,22 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogHeader.vue b/web/src/components/ui/alert-dialog/AlertDialogHeader.vue new file mode 100644 index 0000000..dbe72a7 --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogTitle.vue b/web/src/components/ui/alert-dialog/AlertDialogTitle.vue new file mode 100644 index 0000000..bb97e4d --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogTitle.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/src/components/ui/alert-dialog/AlertDialogTrigger.vue b/web/src/components/ui/alert-dialog/AlertDialogTrigger.vue new file mode 100644 index 0000000..98d40ee --- /dev/null +++ b/web/src/components/ui/alert-dialog/AlertDialogTrigger.vue @@ -0,0 +1,12 @@ + + + diff --git a/web/src/components/ui/alert-dialog/index.ts b/web/src/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..cf1b45d --- /dev/null +++ b/web/src/components/ui/alert-dialog/index.ts @@ -0,0 +1,9 @@ +export { default as AlertDialog } from "./AlertDialog.vue" +export { default as AlertDialogAction } from "./AlertDialogAction.vue" +export { default as AlertDialogCancel } from "./AlertDialogCancel.vue" +export { default as AlertDialogContent } from "./AlertDialogContent.vue" +export { default as AlertDialogDescription } from "./AlertDialogDescription.vue" +export { default as AlertDialogFooter } from "./AlertDialogFooter.vue" +export { default as AlertDialogHeader } from "./AlertDialogHeader.vue" +export { default as AlertDialogTitle } from "./AlertDialogTitle.vue" +export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue" diff --git a/web/src/components/ui/badge/Badge.vue b/web/src/components/ui/badge/Badge.vue new file mode 100644 index 0000000..d894dfe --- /dev/null +++ b/web/src/components/ui/badge/Badge.vue @@ -0,0 +1,26 @@ + + + diff --git a/web/src/components/ui/badge/index.ts b/web/src/components/ui/badge/index.ts new file mode 100644 index 0000000..bbc0dfa --- /dev/null +++ b/web/src/components/ui/badge/index.ts @@ -0,0 +1,26 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Badge } from "./Badge.vue" + +export const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) +export type BadgeVariants = VariantProps diff --git a/web/src/components/ui/button/Button.vue b/web/src/components/ui/button/Button.vue new file mode 100644 index 0000000..374320b --- /dev/null +++ b/web/src/components/ui/button/Button.vue @@ -0,0 +1,29 @@ + + + diff --git a/web/src/components/ui/button/index.ts b/web/src/components/ui/button/index.ts new file mode 100644 index 0000000..26e2c55 --- /dev/null +++ b/web/src/components/ui/button/index.ts @@ -0,0 +1,38 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Button } from "./Button.vue" + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + "default": "h-9 px-4 py-2 has-[>svg]:px-3", + "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + "lg": "h-10 rounded-md px-6 has-[>svg]:px-4", + "icon": "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +) +export type ButtonVariants = VariantProps diff --git a/web/src/components/ui/card/Card.vue b/web/src/components/ui/card/Card.vue new file mode 100644 index 0000000..f5a0707 --- /dev/null +++ b/web/src/components/ui/card/Card.vue @@ -0,0 +1,22 @@ + + + diff --git a/web/src/components/ui/card/CardAction.vue b/web/src/components/ui/card/CardAction.vue new file mode 100644 index 0000000..c91638b --- /dev/null +++ b/web/src/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/card/CardContent.vue b/web/src/components/ui/card/CardContent.vue new file mode 100644 index 0000000..dfbc552 --- /dev/null +++ b/web/src/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/card/CardDescription.vue b/web/src/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..71c1b8d --- /dev/null +++ b/web/src/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/card/CardFooter.vue b/web/src/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..9e3739e --- /dev/null +++ b/web/src/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/card/CardHeader.vue b/web/src/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..4fe4da4 --- /dev/null +++ b/web/src/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/card/CardTitle.vue b/web/src/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..5f479e7 --- /dev/null +++ b/web/src/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/card/index.ts b/web/src/components/ui/card/index.ts new file mode 100644 index 0000000..1627758 --- /dev/null +++ b/web/src/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from "./Card.vue" +export { default as CardAction } from "./CardAction.vue" +export { default as CardContent } from "./CardContent.vue" +export { default as CardDescription } from "./CardDescription.vue" +export { default as CardFooter } from "./CardFooter.vue" +export { default as CardHeader } from "./CardHeader.vue" +export { default as CardTitle } from "./CardTitle.vue" diff --git a/web/src/components/ui/dialog/Dialog.vue b/web/src/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..ade5260 --- /dev/null +++ b/web/src/components/ui/dialog/Dialog.vue @@ -0,0 +1,19 @@ + + + diff --git a/web/src/components/ui/dialog/DialogClose.vue b/web/src/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..c5fae04 --- /dev/null +++ b/web/src/components/ui/dialog/DialogClose.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/components/ui/dialog/DialogContent.vue b/web/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..7f86b47 --- /dev/null +++ b/web/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,53 @@ + + + diff --git a/web/src/components/ui/dialog/DialogDescription.vue b/web/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..f52e655 --- /dev/null +++ b/web/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/ui/dialog/DialogFooter.vue b/web/src/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..0a936e6 --- /dev/null +++ b/web/src/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/components/ui/dialog/DialogHeader.vue b/web/src/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..bfc3c64 --- /dev/null +++ b/web/src/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/dialog/DialogOverlay.vue b/web/src/components/ui/dialog/DialogOverlay.vue new file mode 100644 index 0000000..7790077 --- /dev/null +++ b/web/src/components/ui/dialog/DialogOverlay.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/src/components/ui/dialog/DialogScrollContent.vue b/web/src/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..f2475db --- /dev/null +++ b/web/src/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,59 @@ + + + diff --git a/web/src/components/ui/dialog/DialogTitle.vue b/web/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..860f01a --- /dev/null +++ b/web/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/ui/dialog/DialogTrigger.vue b/web/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..49667e9 --- /dev/null +++ b/web/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/components/ui/dialog/index.ts b/web/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..6768b09 --- /dev/null +++ b/web/src/components/ui/dialog/index.ts @@ -0,0 +1,10 @@ +export { default as Dialog } from "./Dialog.vue" +export { default as DialogClose } from "./DialogClose.vue" +export { default as DialogContent } from "./DialogContent.vue" +export { default as DialogDescription } from "./DialogDescription.vue" +export { default as DialogFooter } from "./DialogFooter.vue" +export { default as DialogHeader } from "./DialogHeader.vue" +export { default as DialogOverlay } from "./DialogOverlay.vue" +export { default as DialogScrollContent } from "./DialogScrollContent.vue" +export { default as DialogTitle } from "./DialogTitle.vue" +export { default as DialogTrigger } from "./DialogTrigger.vue" diff --git a/web/src/components/ui/drawer/Drawer.vue b/web/src/components/ui/drawer/Drawer.vue new file mode 100644 index 0000000..5428e35 --- /dev/null +++ b/web/src/components/ui/drawer/Drawer.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerClose.vue b/web/src/components/ui/drawer/DrawerClose.vue new file mode 100644 index 0000000..eecc11b --- /dev/null +++ b/web/src/components/ui/drawer/DrawerClose.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerContent.vue b/web/src/components/ui/drawer/DrawerContent.vue new file mode 100644 index 0000000..0518c7c --- /dev/null +++ b/web/src/components/ui/drawer/DrawerContent.vue @@ -0,0 +1,38 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerDescription.vue b/web/src/components/ui/drawer/DrawerDescription.vue new file mode 100644 index 0000000..77e69ba --- /dev/null +++ b/web/src/components/ui/drawer/DrawerDescription.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerFooter.vue b/web/src/components/ui/drawer/DrawerFooter.vue new file mode 100644 index 0000000..34c2e12 --- /dev/null +++ b/web/src/components/ui/drawer/DrawerFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerHeader.vue b/web/src/components/ui/drawer/DrawerHeader.vue new file mode 100644 index 0000000..f68aeb0 --- /dev/null +++ b/web/src/components/ui/drawer/DrawerHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerOverlay.vue b/web/src/components/ui/drawer/DrawerOverlay.vue new file mode 100644 index 0000000..e7e4a05 --- /dev/null +++ b/web/src/components/ui/drawer/DrawerOverlay.vue @@ -0,0 +1,19 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerTitle.vue b/web/src/components/ui/drawer/DrawerTitle.vue new file mode 100644 index 0000000..328fb0d --- /dev/null +++ b/web/src/components/ui/drawer/DrawerTitle.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/src/components/ui/drawer/DrawerTrigger.vue b/web/src/components/ui/drawer/DrawerTrigger.vue new file mode 100644 index 0000000..04b5bf1 --- /dev/null +++ b/web/src/components/ui/drawer/DrawerTrigger.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/components/ui/drawer/index.ts b/web/src/components/ui/drawer/index.ts new file mode 100644 index 0000000..0d07820 --- /dev/null +++ b/web/src/components/ui/drawer/index.ts @@ -0,0 +1,9 @@ +export { default as Drawer } from "./Drawer.vue" +export { default as DrawerClose } from "./DrawerClose.vue" +export { default as DrawerContent } from "./DrawerContent.vue" +export { default as DrawerDescription } from "./DrawerDescription.vue" +export { default as DrawerFooter } from "./DrawerFooter.vue" +export { default as DrawerHeader } from "./DrawerHeader.vue" +export { default as DrawerOverlay } from "./DrawerOverlay.vue" +export { default as DrawerTitle } from "./DrawerTitle.vue" +export { default as DrawerTrigger } from "./DrawerTrigger.vue" diff --git a/web/src/components/ui/dropdown-menu/DropdownMenu.vue b/web/src/components/ui/dropdown-menu/DropdownMenu.vue new file mode 100644 index 0000000..e1c9ee3 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenu.vue @@ -0,0 +1,19 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..1253078 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuContent.vue b/web/src/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..7c43014 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..da634ec --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuItem.vue b/web/src/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..f56cae3 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..8bca83c --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..fe82cad --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..e03c40c --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/web/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..1b936c3 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 0000000..60be75c --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..7472e77 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,18 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..d7c6b08 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,27 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..1683aaf --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/web/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..75cd747 --- /dev/null +++ b/web/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/dropdown-menu/index.ts b/web/src/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..955fe3a --- /dev/null +++ b/web/src/components/ui/dropdown-menu/index.ts @@ -0,0 +1,16 @@ +export { default as DropdownMenu } from "./DropdownMenu.vue" + +export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue" +export { default as DropdownMenuContent } from "./DropdownMenuContent.vue" +export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue" +export { default as DropdownMenuItem } from "./DropdownMenuItem.vue" +export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue" +export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue" +export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue" +export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue" +export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue" +export { default as DropdownMenuSub } from "./DropdownMenuSub.vue" +export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue" +export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue" +export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue" +export { DropdownMenuPortal } from "reka-ui" diff --git a/web/src/components/ui/input/Input.vue b/web/src/components/ui/input/Input.vue new file mode 100644 index 0000000..7a2e148 --- /dev/null +++ b/web/src/components/ui/input/Input.vue @@ -0,0 +1,33 @@ + + + diff --git a/web/src/components/ui/input/index.ts b/web/src/components/ui/input/index.ts new file mode 100644 index 0000000..9976b86 --- /dev/null +++ b/web/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from "./Input.vue" diff --git a/web/src/components/ui/label/Label.vue b/web/src/components/ui/label/Label.vue new file mode 100644 index 0000000..ee63970 --- /dev/null +++ b/web/src/components/ui/label/Label.vue @@ -0,0 +1,26 @@ + + + diff --git a/web/src/components/ui/label/index.ts b/web/src/components/ui/label/index.ts new file mode 100644 index 0000000..036e35c --- /dev/null +++ b/web/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from "./Label.vue" diff --git a/web/src/components/ui/radio-group/RadioGroup.vue b/web/src/components/ui/radio-group/RadioGroup.vue new file mode 100644 index 0000000..54045c4 --- /dev/null +++ b/web/src/components/ui/radio-group/RadioGroup.vue @@ -0,0 +1,25 @@ + + + diff --git a/web/src/components/ui/radio-group/RadioGroupItem.vue b/web/src/components/ui/radio-group/RadioGroupItem.vue new file mode 100644 index 0000000..2488c9d --- /dev/null +++ b/web/src/components/ui/radio-group/RadioGroupItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/web/src/components/ui/radio-group/index.ts b/web/src/components/ui/radio-group/index.ts new file mode 100644 index 0000000..7cb0e3b --- /dev/null +++ b/web/src/components/ui/radio-group/index.ts @@ -0,0 +1,2 @@ +export { default as RadioGroup } from "./RadioGroup.vue" +export { default as RadioGroupItem } from "./RadioGroupItem.vue" diff --git a/web/src/components/ui/scroll-area/ScrollArea.vue b/web/src/components/ui/scroll-area/ScrollArea.vue new file mode 100644 index 0000000..6112caa --- /dev/null +++ b/web/src/components/ui/scroll-area/ScrollArea.vue @@ -0,0 +1,33 @@ + + + diff --git a/web/src/components/ui/scroll-area/ScrollBar.vue b/web/src/components/ui/scroll-area/ScrollBar.vue new file mode 100644 index 0000000..a0b6f9b --- /dev/null +++ b/web/src/components/ui/scroll-area/ScrollBar.vue @@ -0,0 +1,32 @@ + + + diff --git a/web/src/components/ui/scroll-area/index.ts b/web/src/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..c416759 --- /dev/null +++ b/web/src/components/ui/scroll-area/index.ts @@ -0,0 +1,2 @@ +export { default as ScrollArea } from "./ScrollArea.vue" +export { default as ScrollBar } from "./ScrollBar.vue" diff --git a/web/src/components/ui/separator/Separator.vue b/web/src/components/ui/separator/Separator.vue new file mode 100644 index 0000000..78d60ec --- /dev/null +++ b/web/src/components/ui/separator/Separator.vue @@ -0,0 +1,29 @@ + + + diff --git a/web/src/components/ui/separator/index.ts b/web/src/components/ui/separator/index.ts new file mode 100644 index 0000000..4407287 --- /dev/null +++ b/web/src/components/ui/separator/index.ts @@ -0,0 +1 @@ +export { default as Separator } from "./Separator.vue" diff --git a/web/src/components/ui/sonner/Sonner.vue b/web/src/components/ui/sonner/Sonner.vue new file mode 100644 index 0000000..71b58e4 --- /dev/null +++ b/web/src/components/ui/sonner/Sonner.vue @@ -0,0 +1,16 @@ + + + diff --git a/web/src/components/ui/sonner/index.ts b/web/src/components/ui/sonner/index.ts new file mode 100644 index 0000000..6673112 --- /dev/null +++ b/web/src/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./Sonner.vue" diff --git a/web/src/components/ui/switch/Switch.vue b/web/src/components/ui/switch/Switch.vue new file mode 100644 index 0000000..2e725ed --- /dev/null +++ b/web/src/components/ui/switch/Switch.vue @@ -0,0 +1,38 @@ + + + diff --git a/web/src/components/ui/switch/index.ts b/web/src/components/ui/switch/index.ts new file mode 100644 index 0000000..cc081f3 --- /dev/null +++ b/web/src/components/ui/switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from "./Switch.vue" diff --git a/web/src/components/ui/table/Table.vue b/web/src/components/ui/table/Table.vue new file mode 100644 index 0000000..0d0cd9b --- /dev/null +++ b/web/src/components/ui/table/Table.vue @@ -0,0 +1,16 @@ + + + diff --git a/web/src/components/ui/table/TableBody.vue b/web/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..d14a2d3 --- /dev/null +++ b/web/src/components/ui/table/TableBody.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/table/TableCaption.vue b/web/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..3630084 --- /dev/null +++ b/web/src/components/ui/table/TableCaption.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/table/TableCell.vue b/web/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..d6e9ed2 --- /dev/null +++ b/web/src/components/ui/table/TableCell.vue @@ -0,0 +1,22 @@ + + + diff --git a/web/src/components/ui/table/TableEmpty.vue b/web/src/components/ui/table/TableEmpty.vue new file mode 100644 index 0000000..9519328 --- /dev/null +++ b/web/src/components/ui/table/TableEmpty.vue @@ -0,0 +1,34 @@ + + + diff --git a/web/src/components/ui/table/TableFooter.vue b/web/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..29e0ce9 --- /dev/null +++ b/web/src/components/ui/table/TableFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/table/TableHead.vue b/web/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..f83efe5 --- /dev/null +++ b/web/src/components/ui/table/TableHead.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/table/TableHeader.vue b/web/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..b4ab5cf --- /dev/null +++ b/web/src/components/ui/table/TableHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/table/TableRow.vue b/web/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..8f1d172 --- /dev/null +++ b/web/src/components/ui/table/TableRow.vue @@ -0,0 +1,17 @@ + + + diff --git a/web/src/components/ui/table/index.ts b/web/src/components/ui/table/index.ts new file mode 100644 index 0000000..3be308b --- /dev/null +++ b/web/src/components/ui/table/index.ts @@ -0,0 +1,9 @@ +export { default as Table } from "./Table.vue" +export { default as TableBody } from "./TableBody.vue" +export { default as TableCaption } from "./TableCaption.vue" +export { default as TableCell } from "./TableCell.vue" +export { default as TableEmpty } from "./TableEmpty.vue" +export { default as TableFooter } from "./TableFooter.vue" +export { default as TableHead } from "./TableHead.vue" +export { default as TableHeader } from "./TableHeader.vue" +export { default as TableRow } from "./TableRow.vue" diff --git a/web/src/components/ui/table/utils.ts b/web/src/components/ui/table/utils.ts new file mode 100644 index 0000000..3d4fd12 --- /dev/null +++ b/web/src/components/ui/table/utils.ts @@ -0,0 +1,10 @@ +import type { Updater } from "@tanstack/vue-table" + +import type { Ref } from "vue" +import { isFunction } from "@tanstack/vue-table" + +export function valueUpdater(updaterOrValue: Updater, ref: Ref) { + ref.value = isFunction(updaterOrValue) + ? updaterOrValue(ref.value) + : updaterOrValue +} diff --git a/web/src/components/ui/tabs/Tabs.vue b/web/src/components/ui/tabs/Tabs.vue new file mode 100644 index 0000000..d260a15 --- /dev/null +++ b/web/src/components/ui/tabs/Tabs.vue @@ -0,0 +1,24 @@ + + + diff --git a/web/src/components/ui/tabs/TabsContent.vue b/web/src/components/ui/tabs/TabsContent.vue new file mode 100644 index 0000000..3186ee8 --- /dev/null +++ b/web/src/components/ui/tabs/TabsContent.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/src/components/ui/tabs/TabsList.vue b/web/src/components/ui/tabs/TabsList.vue new file mode 100644 index 0000000..a64a2da --- /dev/null +++ b/web/src/components/ui/tabs/TabsList.vue @@ -0,0 +1,24 @@ + + + diff --git a/web/src/components/ui/tabs/TabsTrigger.vue b/web/src/components/ui/tabs/TabsTrigger.vue new file mode 100644 index 0000000..45e424f --- /dev/null +++ b/web/src/components/ui/tabs/TabsTrigger.vue @@ -0,0 +1,26 @@ + + + diff --git a/web/src/components/ui/tabs/index.ts b/web/src/components/ui/tabs/index.ts new file mode 100644 index 0000000..7f99b7f --- /dev/null +++ b/web/src/components/ui/tabs/index.ts @@ -0,0 +1,4 @@ +export { default as Tabs } from "./Tabs.vue" +export { default as TabsContent } from "./TabsContent.vue" +export { default as TabsList } from "./TabsList.vue" +export { default as TabsTrigger } from "./TabsTrigger.vue" diff --git a/web/src/components/ui/textarea/Textarea.vue b/web/src/components/ui/textarea/Textarea.vue new file mode 100644 index 0000000..790f10c --- /dev/null +++ b/web/src/components/ui/textarea/Textarea.vue @@ -0,0 +1,28 @@ + + +