udpate
This commit is contained in:
44
.cnb.yml
44
.cnb.yml
@@ -1,44 +0,0 @@
|
|||||||
# .cnb.yml
|
|
||||||
include:
|
|
||||||
- https://cnb.cool/kevisual/cnb/-/blob/main/.cnb/template.yml
|
|
||||||
|
|
||||||
.common_env: &common_env
|
|
||||||
env:
|
|
||||||
TO_REPO: kevisual/router-studio
|
|
||||||
TO_URL: git.xiongxiao.me
|
|
||||||
imports:
|
|
||||||
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
|
||||||
|
|
||||||
$:
|
|
||||||
vscode:
|
|
||||||
- docker:
|
|
||||||
image: docker.cnb.cool/kevisual/dev-env:latest
|
|
||||||
services:
|
|
||||||
- vscode
|
|
||||||
- docker
|
|
||||||
imports: !reference [.common_env, imports]
|
|
||||||
# 开发环境启动后会执行的任务
|
|
||||||
# stages:
|
|
||||||
# - name: pnpm install
|
|
||||||
# script: pnpm install
|
|
||||||
stages: !reference [.dev_tempalte, stages]
|
|
||||||
|
|
||||||
.common_sync_to_gitea: &common_sync_to_gitea
|
|
||||||
- <<: *common_env
|
|
||||||
services: !reference [.common_sync_to_gitea_template, services]
|
|
||||||
stages: !reference [.common_sync_to_gitea_template, stages]
|
|
||||||
|
|
||||||
.common_sync_from_gitea: &common_sync_from_gitea
|
|
||||||
- <<: *common_env
|
|
||||||
services: !reference [.common_sync_from_gitea_template, services]
|
|
||||||
stages: !reference [.common_sync_from_gitea_template, stages]
|
|
||||||
|
|
||||||
main:
|
|
||||||
web_trigger_sync_to_gitea:
|
|
||||||
- <<: *common_sync_to_gitea
|
|
||||||
web_trigger_sync_from_gitea:
|
|
||||||
- <<: *common_sync_from_gitea
|
|
||||||
api_trigger_sync_to_gitea:
|
|
||||||
- <<: *common_sync_to_gitea
|
|
||||||
api_trigger_sync_from_gitea:
|
|
||||||
- <<: *common_sync_from_gitea
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# .cnb/web_trigger.yml
|
|
||||||
branch:
|
|
||||||
# 如下按钮在分支名以 release 开头的分支详情页面显示
|
|
||||||
- reg: "^main"
|
|
||||||
buttons:
|
|
||||||
- name: 同步代码到gitea
|
|
||||||
description: 同步代码到gitea
|
|
||||||
event: web_trigger_sync_to_gitea
|
|
||||||
- name: 同步gitea代码到当前仓库
|
|
||||||
description: 同步gitea代码到当前仓库
|
|
||||||
event: web_trigger_sync_from_gitea
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL='http://localhost:4005'
|
|
||||||
32
backup/web/.github/prompts/astro.prompt.md
vendored
32
backup/web/.github/prompts/astro.prompt.md
vendored
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
agent: agent
|
|
||||||
tags: ["astro", "react", "tailwindcss", "shadcn/ui", "typescript"]
|
|
||||||
createdAt: 2026-01-03
|
|
||||||
---
|
|
||||||
|
|
||||||
# 项目技术栈和上下文
|
|
||||||
|
|
||||||
## 核心框架和库
|
|
||||||
- **Astro** - 静态站点生成框架,用于构建高性能网站
|
|
||||||
- **React** - 用于构建交互式 UI 组件
|
|
||||||
- **TypeScript** - 项目使用 TypeScript 编写,有 tsconfig.json 配置
|
|
||||||
|
|
||||||
## UI 和样式
|
|
||||||
- **TailwindCSS** - CSS 框架,已集成
|
|
||||||
- **shadcn/ui** - 高质量 React 组件库,已安装
|
|
||||||
|
|
||||||
## 项目结构特点
|
|
||||||
- 使用 pnpm 工作区管理
|
|
||||||
- `src/` 目录包含主要源代码
|
|
||||||
- `apps/` - 应用模块(chat、cv、studio、query-view 等)
|
|
||||||
- `components/` - React组件
|
|
||||||
- `pages/` - Astro 页面
|
|
||||||
- `layouts/` - Astro 布局
|
|
||||||
- `slides/` - 演示幻灯片内容
|
|
||||||
|
|
||||||
## 开发指南
|
|
||||||
- 修改代码时遵循项目现有的代码结构和命名约定
|
|
||||||
- React 组件通常使用 `.tsx` 后缀
|
|
||||||
- Astro 组件使用 `.astro` 后缀
|
|
||||||
- 样式优先使用 TailwindCSS 类
|
|
||||||
- 复用已有的 shadcn/ui 组件库中的组件
|
|
||||||
9
backup/web/.gitignore
vendored
9
backup/web/.gitignore
vendored
@@ -1,9 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
.astro
|
|
||||||
|
|
||||||
dist
|
|
||||||
|
|
||||||
.env
|
|
||||||
!.env*example
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"tools": {
|
|
||||||
"skill": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
title: git-commit
|
|
||||||
description: 获取需要diff的代码,总结和提交代码的技能
|
|
||||||
license: MIT
|
|
||||||
compatibility: opencode
|
|
||||||
metadata:
|
|
||||||
audience: maintainers
|
|
||||||
---
|
|
||||||
|
|
||||||
## 我的工作
|
|
||||||
|
|
||||||
- 获取代码变更的diff
|
|
||||||
- 总结代码变更内容
|
|
||||||
- 创建git提交
|
|
||||||
- 推送代码到远程仓库
|
|
||||||
|
|
||||||
## 何时使用我
|
|
||||||
|
|
||||||
在需要提交代码变更时使用我。
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
|
||||||
import mdx from '@astrojs/mdx';
|
|
||||||
import react from '@astrojs/react';
|
|
||||||
import sitemap from '@astrojs/sitemap';
|
|
||||||
import pkgs from './package.json';
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
// import vue from '@astrojs/vue';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
let target = process.env.VITE_API_URL || 'http://localhost:51515';
|
|
||||||
console.log('API Proxy Target:', target);
|
|
||||||
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
|
||||||
let proxy = {
|
|
||||||
'/root/': apiProxy,
|
|
||||||
'/api': apiProxy,
|
|
||||||
'/client': apiProxy,
|
|
||||||
};
|
|
||||||
|
|
||||||
const basename = isDev ? undefined : `${pkgs.basename}`;
|
|
||||||
export default defineConfig({
|
|
||||||
base: basename,
|
|
||||||
integrations: [
|
|
||||||
mdx(),
|
|
||||||
react(), //
|
|
||||||
// vue(),
|
|
||||||
// sitemap(), // sitemap must be site has a domain
|
|
||||||
],
|
|
||||||
server: {
|
|
||||||
port: 7008,
|
|
||||||
host: '0.0.0.0',
|
|
||||||
allowedHosts: true,
|
|
||||||
},
|
|
||||||
vite: {
|
|
||||||
plugins: [tailwindcss()],
|
|
||||||
define: {
|
|
||||||
BASE_NAME: JSON.stringify(basename || ''),
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
proxy,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "new-york",
|
|
||||||
"rsc": false,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "src/styles/global.css",
|
|
||||||
"baseColor": "neutral",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide",
|
|
||||||
"aliases": {
|
|
||||||
"components": "@/components",
|
|
||||||
"utils": "@/lib/utils",
|
|
||||||
"ui": "@/components/ui",
|
|
||||||
"lib": "@/lib",
|
|
||||||
"hooks": "@/hooks"
|
|
||||||
},
|
|
||||||
"registries": {}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "kevisual",
|
|
||||||
"share": "public"
|
|
||||||
},
|
|
||||||
"registry": "https://kevisual.cn/root/ai/kevisual/frontend/simple-astro-template",
|
|
||||||
"clone": {
|
|
||||||
".": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"syncd": [
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"**/*"
|
|
||||||
],
|
|
||||||
"registry": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sync": {
|
|
||||||
".gitignore": {
|
|
||||||
"url": "/gitignore.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@kevisual/router-studio",
|
|
||||||
"version": "0.0.2",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"basename": "/root/router-studio",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "astro dev",
|
|
||||||
"build": "astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"pub": "envision deploy ./dist -k router-studio -v 0.0.2 -u -y yes",
|
|
||||||
"slide:dev": "slidev --open slides/index.md",
|
|
||||||
"slide:build": "slidev build slides/index.md --base /root/router-studio-slide/",
|
|
||||||
"slide:pub": "envision deploy ./slides/dist -k router-studio-slide -v 0.0.2 -u -y yes",
|
|
||||||
"ui": "pnpm dlx shadcn@latest add "
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
|
||||||
"license": "MIT",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/mdx": "^4.3.13",
|
|
||||||
"@astrojs/react": "^4.4.2",
|
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
|
||||||
"@astrojs/vue": "^5.1.3",
|
|
||||||
"@kevisual/cache": "^0.0.5",
|
|
||||||
"@kevisual/context": "^0.0.4",
|
|
||||||
"@kevisual/query": "^0.0.35",
|
|
||||||
"@kevisual/query-login": "^0.0.7",
|
|
||||||
"@kevisual/registry": "^0.0.1",
|
|
||||||
"@kevisual/router": "^0.0.52",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"@uiw/react-md-editor": "^4.0.11",
|
|
||||||
"antd": "^6.1.3",
|
|
||||||
"astro": "^5.16.6",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.1.1",
|
|
||||||
"dayjs": "^1.11.19",
|
|
||||||
"es-toolkit": "^1.43.0",
|
|
||||||
"github-markdown-css": "^5.8.1",
|
|
||||||
"handsontable": "^16.2.0",
|
|
||||||
"highlight.js": "^11.11.1",
|
|
||||||
"lucide-react": "^0.562.0",
|
|
||||||
"marked": "^17.0.1",
|
|
||||||
"marked-highlight": "^2.2.3",
|
|
||||||
"nanoid": "^5.1.6",
|
|
||||||
"papaparse": "^5.5.3",
|
|
||||||
"react": "^19.2.3",
|
|
||||||
"react-dom": "^19.2.3",
|
|
||||||
"react-hook-form": "^7.70.0",
|
|
||||||
"react-resizable-panels": "^4.2.1",
|
|
||||||
"react-toastify": "^11.0.5",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
|
||||||
"vue": "^3.5.26",
|
|
||||||
"zustand": "^5.0.9"
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@kevisual/api": "^0.0.17",
|
|
||||||
"@kevisual/types": "^0.0.10",
|
|
||||||
"@types/react": "^19.2.7",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"baseline-browser-mapping": "^2.9.11",
|
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"tailwindcss": "^4.1.18",
|
|
||||||
"tw-animate-css": "^1.4.0"
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@10.27.0",
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"@tailwindcss/oxide",
|
|
||||||
"esbuild",
|
|
||||||
"sharp"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
8346
backup/web/pnpm-lock.yaml
generated
8346
backup/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
onlyBuiltDependencies:
|
|
||||||
- core-js
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
count: {
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const counter = ref(props.count)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex w-min border border-main rounded-md">
|
|
||||||
<button
|
|
||||||
class="border-r border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
|
||||||
@click="counter -= 1"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<span class="m-auto p-2">{{ counter }}</span>
|
|
||||||
<button
|
|
||||||
class="border-l border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
|
||||||
@click="counter += 1"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,611 +0,0 @@
|
|||||||
# Welcome to Slidev
|
|
||||||
|
|
||||||
Presentation slides for developers
|
|
||||||
|
|
||||||
<div @click="$slidev.nav.next" class="mt-12 py-1" hover:bg="white op-10">
|
|
||||||
Press Space for next page <carbon:arrow-right />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="abs-br m-6 text-xl">
|
|
||||||
<button @click="$slidev.nav.openInEditor()" title="Open in Editor" class="slidev-icon-btn">
|
|
||||||
<carbon:edit />
|
|
||||||
</button>
|
|
||||||
<a href="https://github.com/slidevjs/slidev" target="_blank" class="slidev-icon-btn">
|
|
||||||
<carbon:logo-github />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
The last comment block of each slide will be treated as slide notes. It will be visible and editable in Presenter Mode along with the slide. [Read more in the docs](https://sli.dev/guide/syntax.html#notes)
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
transition: fade-out
|
|
||||||
---
|
|
||||||
|
|
||||||
# What is Slidev?
|
|
||||||
|
|
||||||
Slidev is a slides maker and presenter designed for developers, consist of the following features
|
|
||||||
|
|
||||||
- 📝 **Text-based** - focus on the content with Markdown, and then style them later
|
|
||||||
- 🎨 **Themable** - themes can be shared and re-used as npm packages
|
|
||||||
- 🧑💻 **Developer Friendly** - code highlighting, live coding with autocompletion
|
|
||||||
- 🤹 **Interactive** - embed Vue components to enhance your expressions
|
|
||||||
- 🎥 **Recording** - built-in recording and camera view
|
|
||||||
- 📤 **Portable** - export to PDF, PPTX, PNGs, or even a hostable SPA
|
|
||||||
- 🛠 **Hackable** - virtually anything that's possible on a webpage is possible in Slidev
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
Read more about [Why Slidev?](https://sli.dev/guide/why)
|
|
||||||
|
|
||||||
<!--
|
|
||||||
You can have `style` tag in markdown to override the style for the current page.
|
|
||||||
Learn more: https://sli.dev/features/slide-scope-style
|
|
||||||
-->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
h1 {
|
|
||||||
background-color: #2B90B6;
|
|
||||||
background-image: linear-gradient(45deg, #4EC5D4 10%, #146b8c 20%);
|
|
||||||
background-size: 100%;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-moz-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
-moz-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Here is another comment.
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
transition: slide-up
|
|
||||||
level: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
# Navigation
|
|
||||||
|
|
||||||
Hover on the bottom-left corner to see the navigation's controls panel, [learn more](https://sli.dev/guide/ui#navigation-bar)
|
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
|
||||||
|
|
||||||
| | |
|
|
||||||
| --------------------------------------------------- | --------------------------- |
|
|
||||||
| <kbd>right</kbd> / <kbd>space</kbd> | next animation or slide |
|
|
||||||
| <kbd>left</kbd> / <kbd>shift</kbd><kbd>space</kbd> | previous animation or slide |
|
|
||||||
| <kbd>up</kbd> | previous slide |
|
|
||||||
| <kbd>down</kbd> | next slide |
|
|
||||||
|
|
||||||
<!-- https://sli.dev/guide/animations.html#click-animation -->
|
|
||||||
<img
|
|
||||||
v-click
|
|
||||||
class="absolute -bottom-9 -left-7 w-80 opacity-50"
|
|
||||||
src="https://sli.dev/assets/arrow-bottom-left.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<p v-after class="absolute bottom-23 left-45 opacity-30 transform -rotate-10">Here!</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
layout: two-cols
|
|
||||||
layoutClass: gap-16
|
|
||||||
---
|
|
||||||
|
|
||||||
# Table of contents
|
|
||||||
|
|
||||||
You can use the `Toc` component to generate a table of contents for your slides:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<Toc minDepth="1" maxDepth="1" />
|
|
||||||
```
|
|
||||||
|
|
||||||
The title will be inferred from your slide content, or you can override it with `title` and `level` in your frontmatter.
|
|
||||||
|
|
||||||
::right::
|
|
||||||
|
|
||||||
<Toc text-sm minDepth="1" maxDepth="2" />
|
|
||||||
|
|
||||||
---
|
|
||||||
layout: image-right
|
|
||||||
image: https://cover.sli.dev
|
|
||||||
---
|
|
||||||
|
|
||||||
# Code
|
|
||||||
|
|
||||||
Use code snippets and get the highlighting directly, and even types hover!
|
|
||||||
|
|
||||||
```ts [filename-example.ts] {all|4|6|6-7|9|all} twoslash
|
|
||||||
// TwoSlash enables TypeScript hover information
|
|
||||||
// and errors in markdown code blocks
|
|
||||||
// More at https://shiki.style/packages/twoslash
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
const doubled = computed(() => count.value * 2)
|
|
||||||
|
|
||||||
doubled.value = 2
|
|
||||||
```
|
|
||||||
|
|
||||||
<arrow v-click="[4, 5]" x1="350" y1="310" x2="195" y2="342" color="#953" width="2" arrowSize="1" />
|
|
||||||
|
|
||||||
<!-- This allow you to embed external code blocks -->
|
|
||||||
<!-- <<< @/snippets/external.ts#snippet -->
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
|
|
||||||
[Learn more](https://sli.dev/features/line-highlighting)
|
|
||||||
|
|
||||||
<!-- Inline style -->
|
|
||||||
<style>
|
|
||||||
.footnotes-sep {
|
|
||||||
@apply mt-5 opacity-10;
|
|
||||||
}
|
|
||||||
.footnotes {
|
|
||||||
@apply text-sm opacity-75;
|
|
||||||
}
|
|
||||||
.footnote-backref {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Notes can also sync with clicks
|
|
||||||
|
|
||||||
[click] This will be highlighted after the first click
|
|
||||||
|
|
||||||
[click] Highlighted with `count = ref(0)`
|
|
||||||
|
|
||||||
[click:3] Last click (skip two clicks)
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
level: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
# Shiki Magic Move
|
|
||||||
|
|
||||||
Powered by [shiki-magic-move](https://shiki-magic-move.netlify.app/), Slidev supports animations across multiple code snippets.
|
|
||||||
|
|
||||||
Add multiple code blocks and wrap them with <code>````md magic-move</code> (four backticks) to enable the magic move. For example:
|
|
||||||
|
|
||||||
````md magic-move {lines: true}
|
|
||||||
```ts {*|2|*}
|
|
||||||
// step 1
|
|
||||||
const author = reactive({
|
|
||||||
name: 'John Doe',
|
|
||||||
books: [
|
|
||||||
'Vue 2 - Advanced Guide',
|
|
||||||
'Vue 3 - Basic Guide',
|
|
||||||
'Vue 4 - The Mystery'
|
|
||||||
]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts {*|1-2|3-4|3-4,8}
|
|
||||||
// step 2
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
author: {
|
|
||||||
name: 'John Doe',
|
|
||||||
books: [
|
|
||||||
'Vue 2 - Advanced Guide',
|
|
||||||
'Vue 3 - Basic Guide',
|
|
||||||
'Vue 4 - The Mystery'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// step 3
|
|
||||||
export default {
|
|
||||||
data: () => ({
|
|
||||||
author: {
|
|
||||||
name: 'John Doe',
|
|
||||||
books: [
|
|
||||||
'Vue 2 - Advanced Guide',
|
|
||||||
'Vue 3 - Basic Guide',
|
|
||||||
'Vue 4 - The Mystery'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Non-code blocks are ignored.
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- step 4 -->
|
|
||||||
<script setup>
|
|
||||||
const author = {
|
|
||||||
name: 'John Doe',
|
|
||||||
books: [
|
|
||||||
'Vue 2 - Advanced Guide',
|
|
||||||
'Vue 3 - Basic Guide',
|
|
||||||
'Vue 4 - The Mystery'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
````
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Components
|
|
||||||
|
|
||||||
<div grid="~ cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
You can use Vue components directly inside your slides.
|
|
||||||
|
|
||||||
We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. And adding your custom components is also super easy.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<Counter :count="10" />
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- ../components/Counter.vue -->
|
|
||||||
<Counter :count="10" m="t-4" />
|
|
||||||
|
|
||||||
Check out [the guides](https://sli.dev/builtin/components.html) for more.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
```html
|
|
||||||
<Tweet id="1390115482657726468" />
|
|
||||||
```
|
|
||||||
|
|
||||||
<Tweet id="1390115482657726468" scale="0.65" />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Presenter note with **bold**, *italic*, and ~~striked~~ text.
|
|
||||||
|
|
||||||
Also, HTML elements are valid:
|
|
||||||
<div class="flex w-full">
|
|
||||||
<span style="flex-grow: 1;">Left content</span>
|
|
||||||
<span>Right content</span>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
---
|
|
||||||
class: px-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# Themes
|
|
||||||
|
|
||||||
Slidev comes with powerful theming support. Themes can provide styles, layouts, components, or even configurations for tools. Switching between themes by just **one edit** in your frontmatter:
|
|
||||||
|
|
||||||
<div grid="~ cols-2 gap-2" m="t-2">
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
theme: default
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
theme: seriph
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-default/01.png?raw=true" alt="">
|
|
||||||
|
|
||||||
<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-seriph/01.png?raw=true" alt="">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Read more about [How to use a theme](https://sli.dev/guide/theme-addon#use-theme) and
|
|
||||||
check out the [Awesome Themes Gallery](https://sli.dev/resources/theme-gallery).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Clicks Animations
|
|
||||||
|
|
||||||
You can add `v-click` to elements to add a click animation.
|
|
||||||
|
|
||||||
<div v-click>
|
|
||||||
|
|
||||||
This shows up when you click the slide:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div v-click>This shows up when you click the slide.</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<v-click>
|
|
||||||
|
|
||||||
The <span v-mark.red="3"><code>v-mark</code> directive</span>
|
|
||||||
also allows you to add
|
|
||||||
<span v-mark.circle.orange="4">inline marks</span>
|
|
||||||
, powered by [Rough Notation](https://roughnotation.com/):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<span v-mark.underline.orange>inline markers</span>
|
|
||||||
```
|
|
||||||
|
|
||||||
</v-click>
|
|
||||||
|
|
||||||
<div mt-20 v-click>
|
|
||||||
|
|
||||||
[Learn more](https://sli.dev/guide/animations#click-animation)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Motions
|
|
||||||
|
|
||||||
Motion animations are powered by [@vueuse/motion](https://motion.vueuse.org/), triggered by `v-motion` directive.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div
|
|
||||||
v-motion
|
|
||||||
:initial="{ x: -80 }"
|
|
||||||
:enter="{ x: 0 }"
|
|
||||||
:click-3="{ x: 80 }"
|
|
||||||
:leave="{ x: 1000 }"
|
|
||||||
>
|
|
||||||
Slidev
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
<div class="w-60 relative">
|
|
||||||
<div class="relative w-40 h-40">
|
|
||||||
<img
|
|
||||||
v-motion
|
|
||||||
:initial="{ x: 800, y: -100, scale: 1.5, rotate: -50 }"
|
|
||||||
:enter="final"
|
|
||||||
class="absolute inset-0"
|
|
||||||
src="https://sli.dev/logo-square.png"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-motion
|
|
||||||
:initial="{ y: 500, x: -100, scale: 2 }"
|
|
||||||
:enter="final"
|
|
||||||
class="absolute inset-0"
|
|
||||||
src="https://sli.dev/logo-circle.png"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-motion
|
|
||||||
:initial="{ x: 600, y: 400, scale: 2, rotate: 100 }"
|
|
||||||
:enter="final"
|
|
||||||
class="absolute inset-0"
|
|
||||||
src="https://sli.dev/logo-triangle.png"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="text-5xl absolute top-14 left-40 text-[#2B90B6] -z-1"
|
|
||||||
v-motion
|
|
||||||
:initial="{ x: -80, opacity: 0}"
|
|
||||||
:enter="{ x: 0, opacity: 1, transition: { delay: 2000, duration: 1000 } }">
|
|
||||||
Slidev
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- vue script setup scripts can be directly used in markdown, and will only affects current page -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
const final = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
rotate: 0,
|
|
||||||
scale: 1,
|
|
||||||
transition: {
|
|
||||||
type: 'spring',
|
|
||||||
damping: 10,
|
|
||||||
stiffness: 20,
|
|
||||||
mass: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-motion
|
|
||||||
:initial="{ x:35, y: 30, opacity: 0}"
|
|
||||||
:enter="{ y: 0, opacity: 1, transition: { delay: 3500 } }">
|
|
||||||
|
|
||||||
[Learn more](https://sli.dev/guide/animations.html#motion)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# LaTeX
|
|
||||||
|
|
||||||
LaTeX is supported out-of-box. Powered by [KaTeX](https://katex.org/).
|
|
||||||
|
|
||||||
<div h-3 />
|
|
||||||
|
|
||||||
Inline $\sqrt{3x-1}+(1+x)^2$
|
|
||||||
|
|
||||||
Block
|
|
||||||
$$ {1|3|all}
|
|
||||||
\begin{aligned}
|
|
||||||
\nabla \cdot \vec{E} &= \frac{\rho}{\varepsilon_0} \\
|
|
||||||
\nabla \cdot \vec{B} &= 0 \\
|
|
||||||
\nabla \times \vec{E} &= -\frac{\partial\vec{B}}{\partial t} \\
|
|
||||||
\nabla \times \vec{B} &= \mu_0\vec{J} + \mu_0\varepsilon_0\frac{\partial\vec{E}}{\partial t}
|
|
||||||
\end{aligned}
|
|
||||||
$$
|
|
||||||
|
|
||||||
[Learn more](https://sli.dev/features/latex)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Diagrams
|
|
||||||
|
|
||||||
You can create diagrams / graphs from textual descriptions, directly in your Markdown.
|
|
||||||
|
|
||||||
<div class="grid grid-cols-4 gap-5 pt-4 -mb-6">
|
|
||||||
|
|
||||||
```mermaid {scale: 0.5, alt: 'A simple sequence diagram'}
|
|
||||||
sequenceDiagram
|
|
||||||
Alice->John: Hello John, how are you?
|
|
||||||
Note over Alice,John: A typical interaction
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid {theme: 'neutral', scale: 0.8}
|
|
||||||
graph TD
|
|
||||||
B[Text] --> C{Decision}
|
|
||||||
C -->|One| D[Result 1]
|
|
||||||
C -->|Two| E[Result 2]
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
mindmap
|
|
||||||
root((mindmap))
|
|
||||||
Origins
|
|
||||||
Long history
|
|
||||||
::icon(fa fa-book)
|
|
||||||
Popularisation
|
|
||||||
British popular psychology author Tony Buzan
|
|
||||||
Research
|
|
||||||
On effectiveness<br/>and features
|
|
||||||
On Automatic creation
|
|
||||||
Uses
|
|
||||||
Creative techniques
|
|
||||||
Strategic planning
|
|
||||||
Argument mapping
|
|
||||||
Tools
|
|
||||||
Pen and paper
|
|
||||||
Mermaid
|
|
||||||
```
|
|
||||||
|
|
||||||
```plantuml {scale: 0.7}
|
|
||||||
@startuml
|
|
||||||
|
|
||||||
package "Some Group" {
|
|
||||||
HTTP - [First Component]
|
|
||||||
[Another Component]
|
|
||||||
}
|
|
||||||
|
|
||||||
node "Other Groups" {
|
|
||||||
FTP - [Second Component]
|
|
||||||
[First Component] --> FTP
|
|
||||||
}
|
|
||||||
|
|
||||||
cloud {
|
|
||||||
[Example 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
database "MySql" {
|
|
||||||
folder "This is my folder" {
|
|
||||||
[Folder 3]
|
|
||||||
}
|
|
||||||
frame "Foo" {
|
|
||||||
[Frame 4]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Another Component] --> [Example 1]
|
|
||||||
[Example 1] --> [Folder 3]
|
|
||||||
[Folder 3] --> [Frame 4]
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
```
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Learn more: [Mermaid Diagrams](https://sli.dev/features/mermaid) and [PlantUML Diagrams](https://sli.dev/features/plantuml)
|
|
||||||
|
|
||||||
---
|
|
||||||
foo: bar
|
|
||||||
dragPos:
|
|
||||||
square: 691,32,167,_,-16
|
|
||||||
---
|
|
||||||
|
|
||||||
# Draggable Elements
|
|
||||||
|
|
||||||
Double-click on the draggable elements to edit their positions.
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
###### Directive Usage
|
|
||||||
|
|
||||||
```md
|
|
||||||
<img v-drag="'square'" src="https://sli.dev/logo.png">
|
|
||||||
```
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
###### Component Usage
|
|
||||||
|
|
||||||
```md
|
|
||||||
<v-drag text-3xl>
|
|
||||||
<div class="i-carbon:arrow-up" />
|
|
||||||
Use the `v-drag` component to have a draggable container!
|
|
||||||
</v-drag>
|
|
||||||
```
|
|
||||||
|
|
||||||
<v-drag pos="640,212,261,_,-15">
|
|
||||||
<div text-center text-3xl border border-main rounded>
|
|
||||||
Double-click me!
|
|
||||||
</div>
|
|
||||||
</v-drag>
|
|
||||||
|
|
||||||
<img v-drag="'square'" src="https://sli.dev/logo.png">
|
|
||||||
|
|
||||||
###### Draggable Arrow
|
|
||||||
|
|
||||||
```md
|
|
||||||
<v-drag-arrow two-way />
|
|
||||||
```
|
|
||||||
|
|
||||||
<v-drag-arrow pos="360,319,253,46" two-way op70 />
|
|
||||||
|
|
||||||
---
|
|
||||||
src: ./pages/imported-slides.md
|
|
||||||
hide: false
|
|
||||||
---
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Monaco Editor
|
|
||||||
|
|
||||||
Slidev provides built-in Monaco Editor support.
|
|
||||||
|
|
||||||
Add `{monaco}` to the code block to turn it into an editor:
|
|
||||||
|
|
||||||
```ts {monaco}
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { emptyArray } from './external'
|
|
||||||
|
|
||||||
const arr = ref(emptyArray(10))
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `{monaco-run}` to create an editor that can execute the code directly in the slide:
|
|
||||||
|
|
||||||
```ts {monaco-run}
|
|
||||||
import { version } from 'vue'
|
|
||||||
import { emptyArray, sayHello } from './external'
|
|
||||||
|
|
||||||
sayHello()
|
|
||||||
console.log(`vue ${version}`)
|
|
||||||
console.log(emptyArray<number>(10).reduce(fib => [...fib, fib.at(-1)! + fib.at(-2)!], [1, 1]))
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
layout: center
|
|
||||||
class: text-center
|
|
||||||
---
|
|
||||||
|
|
||||||
# Learn More
|
|
||||||
|
|
||||||
[Documentation](https://sli.dev) · [GitHub](https://github.com/slidevjs/slidev) · [Showcases](https://sli.dev/resources/showcases)
|
|
||||||
|
|
||||||
<PoweredBySlidev mt-10 />
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
theme: default
|
|
||||||
# random image from a curated Unsplash collection by Anthony
|
|
||||||
background: https://cover.sli.dev
|
|
||||||
# 介绍文档: https://sli.dev
|
|
||||||
title: Welcome to Slidev
|
|
||||||
info: |
|
|
||||||
## 关于Slidev的介绍
|
|
||||||
演示稿
|
|
||||||
class: text-center
|
|
||||||
# https://sli.dev/features/drawing
|
|
||||||
drawings:
|
|
||||||
persist: false
|
|
||||||
# slide transition: https://sli.dev/guide/animations.html#slide-transitions
|
|
||||||
transition: slide-left
|
|
||||||
# enable MDC Syntax: https://sli.dev/features/mdc
|
|
||||||
mdc: true
|
|
||||||
htmlAttrs:
|
|
||||||
dir: ltr
|
|
||||||
lang: zh-CN
|
|
||||||
# duration of the presentation
|
|
||||||
duration: 35min
|
|
||||||
---
|
|
||||||
# slide 是一个 所见即所得的幻灯片制作工具
|
|
||||||
---
|
|
||||||
src: ./demos/contents.md
|
|
||||||
hide: false
|
|
||||||
---
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: '例子'
|
|
||||||
---
|
|
||||||
|
|
||||||
# 常用语法结构
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
# 第二个
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { app } from '@/index.ts'
|
|
||||||
import { useStudioStore } from '../studio/store';
|
|
||||||
import { useShallow } from 'zustand/shallow';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { query } from '@/modules/query.ts'
|
|
||||||
import { QueryViewMessages } from '../query-view';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
export const Chat = () => {
|
|
||||||
const studioStore = useStudioStore(useShallow((state) => ({
|
|
||||||
routes: state.routes,
|
|
||||||
showRightPanel: state.showRightPanel,
|
|
||||||
setShowRightPanel: state.setShowRightPanel,
|
|
||||||
addMessage: state.addMessage,
|
|
||||||
})));
|
|
||||||
const [text, setText] = useState('');
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const onSend = async () => {
|
|
||||||
if (!text.trim() || isLoading) return;
|
|
||||||
setIsLoading(true);
|
|
||||||
const { routes } = studioStore;
|
|
||||||
let callPrompts = '';
|
|
||||||
const toolsList = routes.map((r, index) =>
|
|
||||||
`${index + 1}. 工具名称: ${r.id}\n 描述: ${r.description}`
|
|
||||||
).join('\n\n');
|
|
||||||
|
|
||||||
callPrompts = `你是一个 AI 助手,你可以使用以下工具来帮助用户完成任务:
|
|
||||||
|
|
||||||
${toolsList}
|
|
||||||
|
|
||||||
## 回复规则
|
|
||||||
1. 如果用户的请求可以使用上述工具完成,请返回 JSON 格式数据
|
|
||||||
2. 如果没有合适的工具,请直接分析并回答用户问题
|
|
||||||
|
|
||||||
## JSON 数据格式
|
|
||||||
\`\`\`json
|
|
||||||
{
|
|
||||||
"id": "工具的id",
|
|
||||||
"payload": {
|
|
||||||
// 工具所需的参数(如果需要)
|
|
||||||
// 例如: "id": "xxx", "name": "xxx"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
注意:
|
|
||||||
- payload 中包含工具执行所需的所有参数
|
|
||||||
- 如果工具不需要参数,payload 可以为空对象 {}
|
|
||||||
- 确保返回的 id 与上述工具列表中的工具名称完全匹配`
|
|
||||||
|
|
||||||
const res = await query.post({
|
|
||||||
path: 'ai',
|
|
||||||
payload: {
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: callPrompts
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: text
|
|
||||||
}
|
|
||||||
],
|
|
||||||
isJson: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setText('');
|
|
||||||
console.log('发送消息', text, res);
|
|
||||||
if (res.code === 200) {
|
|
||||||
// 处理返回结果
|
|
||||||
const payload = res.data?.action;
|
|
||||||
if (payload) {
|
|
||||||
const route = routes.find(r => r.id === payload.id);
|
|
||||||
const { path, key } = route || {};
|
|
||||||
const { id, ...otherParams } = payload.payload || {};
|
|
||||||
const action = { path, key, ...otherParams }
|
|
||||||
let response;
|
|
||||||
if (route) {
|
|
||||||
response = await app.run(action);
|
|
||||||
// toast.success('工具调用成功');
|
|
||||||
} else {
|
|
||||||
console.error('未找到对应工具', payload.id);
|
|
||||||
toast.error('未找到对应工具');
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (route?.metadata?.viewItem) {
|
|
||||||
// 自动打开右侧面板
|
|
||||||
if (!studioStore.showRightPanel) {
|
|
||||||
studioStore.setShowRightPanel(true);
|
|
||||||
}
|
|
||||||
const viewItem = route.metadata.viewItem;
|
|
||||||
viewItem.response = response;
|
|
||||||
viewItem.action = action;
|
|
||||||
viewItem.description = route.description || viewItem.description;
|
|
||||||
// @ts-ignore
|
|
||||||
viewItem._id = Date.now();
|
|
||||||
studioStore.addMessage(viewItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
return <div className="h-full flex flex-col border-l border-gray-300 bg-white">
|
|
||||||
<div style={{ height: '3rem' }} className="flex items-center justify-between px-4 border-b border-gray-300 bg-gray-50">
|
|
||||||
<div className="text-sm text-gray-600">智能体</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ height: 'calc(100% - 3rem)' }} className="overflow-auto">
|
|
||||||
<QueryViewMessages type="component" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 px-4 py-3 border-t border-gray-300 bg-white">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="输入消息..."
|
|
||||||
value={text}
|
|
||||||
onChange={(e) => setText(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && onSend()}
|
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md bg-white text-black placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-800 transition-all"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={onSend}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="px-4 py-2 bg-black hover:bg-gray-900 disabled:bg-gray-400 text-white font-medium rounded-md transition-colors duration-200 flex-shrink-0"
|
|
||||||
>
|
|
||||||
{isLoading ? '发送中...' : '发送'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import MDEditor from '@uiw/react-md-editor'
|
|
||||||
import { ToastContainer, Slide } from 'react-toastify'
|
|
||||||
import { Save, Download, Printer, Eye, Edit, RotateCcw } from 'lucide-react'
|
|
||||||
import 'github-markdown-css/github-markdown-light.css'
|
|
||||||
import './index.css'
|
|
||||||
|
|
||||||
const defaultResume = `# 张三
|
|
||||||
|
|
||||||
**前端开发工程师** | 北京 | zhangsan@example.com | 138-0000-0000
|
|
||||||
|
|
||||||
## 个人简介
|
|
||||||
|
|
||||||
热爱前端开发,拥有5年React和Vue开发经验。专注于构建高性能、可维护的Web应用。
|
|
||||||
|
|
||||||
## 工作经历
|
|
||||||
|
|
||||||
### 高级前端工程师 | ABC科技有限公司
|
|
||||||
*2021年6月 - 至今*
|
|
||||||
|
|
||||||
- 负责公司核心产品的前端架构设计和开发
|
|
||||||
- 使用React和TypeScript构建企业级管理系统
|
|
||||||
- 优化前端性能,页面加载速度提升40%
|
|
||||||
- 带领5人前端团队,制定代码规范和最佳实践
|
|
||||||
|
|
||||||
### 前端开发工程师 | XYZ互联网公司
|
|
||||||
*2019年3月 - 2021年5月*
|
|
||||||
|
|
||||||
- 参与电商平台的前端开发
|
|
||||||
- 使用Vue.js开发响应式Web应用
|
|
||||||
- 与后端团队协作,实现RESTful API对接
|
|
||||||
|
|
||||||
## 技能
|
|
||||||
|
|
||||||
- **前端框架**: React, Vue.js, Next.js, Astro
|
|
||||||
- **编程语言**: TypeScript, JavaScript, HTML5, CSS3
|
|
||||||
- **工具**: Git, Webpack, Vite, Docker
|
|
||||||
- **其他**: Node.js, GraphQL, Tailwind CSS
|
|
||||||
|
|
||||||
## 教育背景
|
|
||||||
|
|
||||||
### 计算机科学与技术 | 本科
|
|
||||||
*北京某大学 | 2015年9月 - 2019年6月*
|
|
||||||
|
|
||||||
- 主修课程:数据结构、算法、计算机网络、数据库原理
|
|
||||||
- 优秀毕业生,GPA 3.8/4.0
|
|
||||||
|
|
||||||
## 项目经验
|
|
||||||
|
|
||||||
### 企业级管理系统
|
|
||||||
*技术栈: React, TypeScript, Ant Design*
|
|
||||||
|
|
||||||
- 设计并实现模块化权限管理系统
|
|
||||||
- 开发数据可视化大屏,支持实时数据展示
|
|
||||||
- 编写单元测试,代码覆盖率达到85%
|
|
||||||
|
|
||||||
### 电商平台前端
|
|
||||||
*技术栈: Vue.js, Vuex, Element UI*
|
|
||||||
|
|
||||||
- 实现购物车、订单管理等核心功能
|
|
||||||
- 优化首屏加载时间,LCP从2.5s降至1.2s
|
|
||||||
- 响应式设计,完美支持移动端访问
|
|
||||||
|
|
||||||
## 语言能力
|
|
||||||
|
|
||||||
- 英语:CET-6,能够阅读英文技术文档
|
|
||||||
- 普通话:母语
|
|
||||||
`
|
|
||||||
|
|
||||||
export const AppProvider = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<App />
|
|
||||||
<ToastContainer transition={Slide} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const App = () => {
|
|
||||||
const [markdown, setMarkdown] = useState<string | undefined>(defaultResume)
|
|
||||||
const [isPreviewMode, setIsPreviewMode] = useState(false)
|
|
||||||
|
|
||||||
// 从 localStorage 加载保存的简历
|
|
||||||
useEffect(() => {
|
|
||||||
const saved = localStorage.getItem('cv-content')
|
|
||||||
if (saved) {
|
|
||||||
setMarkdown(saved)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 保存到 localStorage
|
|
||||||
const handleSave = () => {
|
|
||||||
if (markdown) {
|
|
||||||
localStorage.setItem('cv-content', markdown)
|
|
||||||
alert('简历已保存!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出为 Markdown 文件
|
|
||||||
const handleExport = () => {
|
|
||||||
if (markdown) {
|
|
||||||
const blob = new Blob([markdown], { type: 'text/markdown' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = 'resume.md'
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印简历
|
|
||||||
const handlePrint = () => {
|
|
||||||
window.print()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置为默认模板
|
|
||||||
const handleReset = () => {
|
|
||||||
if (confirm('确定要重置为默认模板吗?当前内容将丢失。')) {
|
|
||||||
setMarkdown(defaultResume)
|
|
||||||
localStorage.removeItem('cv-content')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="cv-app">
|
|
||||||
<header className="cv-header">
|
|
||||||
<h1>📄 简历编辑器</h1>
|
|
||||||
<div className="cv-actions">
|
|
||||||
<button onClick={handleSave} className="cv-btn cv-btn-primary">
|
|
||||||
<Save size={16} />
|
|
||||||
<span>保存</span>
|
|
||||||
</button>
|
|
||||||
<button onClick={handleExport} className="cv-btn cv-btn-secondary">
|
|
||||||
<Download size={16} />
|
|
||||||
<span>导出 MD</span>
|
|
||||||
</button>
|
|
||||||
<button onClick={handlePrint} className="cv-btn cv-btn-secondary">
|
|
||||||
<Printer size={16} />
|
|
||||||
<span>打印</span>
|
|
||||||
</button>
|
|
||||||
<button onClick={() => setIsPreviewMode(!isPreviewMode)} className="cv-btn cv-btn-secondary">
|
|
||||||
{isPreviewMode ? <Edit size={16} /> : <Eye size={16} />}
|
|
||||||
<span>{isPreviewMode ? '编辑' : '预览'}</span>
|
|
||||||
</button>
|
|
||||||
<button onClick={handleReset} className="cv-btn cv-btn-danger">
|
|
||||||
<RotateCcw size={16} />
|
|
||||||
<span>重置</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{isPreviewMode ? (
|
|
||||||
<div className="cv-preview-container">
|
|
||||||
<div className="cv-preview markdown-body">
|
|
||||||
<MDEditor.Markdown source={markdown} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="cv-editor-container" data-color-mode="light">
|
|
||||||
<MDEditor
|
|
||||||
value={markdown}
|
|
||||||
onChange={setMarkdown}
|
|
||||||
height={800}
|
|
||||||
preview="edit"
|
|
||||||
textareaProps={{
|
|
||||||
placeholder: '在这里输入你的简历内容(支持 Markdown 格式)...'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { wrapBasename } from "@/modules/basename"
|
|
||||||
|
|
||||||
export const Footer = () => {
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
href: wrapBasename('/'),
|
|
||||||
label: '主页',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: wrapBasename('/docs'),
|
|
||||||
label: '文档',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<footer className="fixed bottom-0 w-full bg-white border-t border-gray-200 shadow-lg">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
||||||
{/* 链接区域 */}
|
|
||||||
<nav className="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-3">
|
|
||||||
{links.map((link) => (
|
|
||||||
<a
|
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
className="relative px-4 py-2 text-sm sm:text-base font-medium text-gray-600 hover:text-blue-600 transition-all duration-300 ease-in-out
|
|
||||||
before:absolute before:bottom-0 before:left-0 before:w-0 before:h-0.5 before:bg-blue-600 before:transition-all before:duration-300
|
|
||||||
hover:before:w-full active:scale-95"
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* 版权信息 */}
|
|
||||||
<div className="text-center text-xs sm:text-sm text-gray-500">
|
|
||||||
© 2025 Daily Question
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
export type MenuProps = {
|
|
||||||
items: MenuItem[];
|
|
||||||
basename?: string;
|
|
||||||
};
|
|
||||||
export type MenuItem = {
|
|
||||||
id: string;
|
|
||||||
data: {
|
|
||||||
title: string;
|
|
||||||
tags: string[];
|
|
||||||
hideInMenu?: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const Menu = (props: MenuProps) => {
|
|
||||||
const { items, basename = '' } = props;
|
|
||||||
const list = useMemo(() => {
|
|
||||||
return items.filter(item => !item.data?.hideInMenu);
|
|
||||||
}, [items]);
|
|
||||||
if (list.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<nav className='flex-1 overflow-y-auto scrollbar bg-sidebar border border-sidebar-border rounded-lg p-4 shadow-sm'>
|
|
||||||
<h2 className="text-sm font-semibold text-sidebar-foreground">列表</h2>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{list.map(item => (
|
|
||||||
<a
|
|
||||||
key={item.id}
|
|
||||||
href={`${basename}/docs/${item.id}/`}
|
|
||||||
className="group block rounded-md hover:bg-sidebar-accent transition-all duration-200 ease-in-out"
|
|
||||||
>
|
|
||||||
<div className="px-3 py-2.5">
|
|
||||||
<h3 className="text-sm font-semibold text-sidebar-foreground group-hover:text-sidebar-accent-foreground transition-colors">
|
|
||||||
{item.data?.title}
|
|
||||||
</h3>
|
|
||||||
{item.data?.tags && item.data.tags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
||||||
{item.data.tags.map(tag => (
|
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className="text-xs px-2 py-0.5 rounded-full bg-muted/60 text-muted-foreground font-medium hover:bg-muted transition-colors"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
import { QueryProxy, RouterViewItem } from '@kevisual/api/proxy'
|
|
||||||
import { app } from '@/index.ts'
|
|
||||||
import { use, useEffect, useState } from 'react'
|
|
||||||
import { flexRender, useReactTable, getCoreRowModel, ColumnDef } from '@tanstack/react-table'
|
|
||||||
import { RefreshCw, Info, MoreVertical, Edit, Trash2, Download, Save, ExternalLink, Code } from 'lucide-react'
|
|
||||||
import { toast, ToastContainer, Slide } from 'react-toastify'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import { useStudioStore } from '../studio/store'
|
|
||||||
import { useShallow } from 'zustand/shallow'
|
|
||||||
import { cloneDeep } from 'es-toolkit'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
type: 'component' | 'page',
|
|
||||||
viewData?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryProxy = new QueryProxy({
|
|
||||||
router: app as any
|
|
||||||
});
|
|
||||||
export const QueryView = (props: Props) => {
|
|
||||||
const [data, setData] = useState<any[]>([])
|
|
||||||
const [columns, setColumns] = useState<ColumnDef<any>[]>([])
|
|
||||||
const [type] = useState<'component' | 'page'>(props.type || 'page')
|
|
||||||
const [viewData, setViewData] = useState<RouterViewItem | null>(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [showMoreMenu, setShowMoreMenu] = useState(false)
|
|
||||||
const [selectedRow, setSelectedRow] = useState<any | null>(null)
|
|
||||||
const [isList, setIsList] = useState(true)
|
|
||||||
const [obj, setObj] = useState<any>(null)
|
|
||||||
const table = useReactTable({
|
|
||||||
data,
|
|
||||||
columns: columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
})
|
|
||||||
const studioStore = useStudioStore(useShallow((state) => ({
|
|
||||||
deleteMessage: state.deleteMessage
|
|
||||||
})))
|
|
||||||
const main = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true)
|
|
||||||
const res = await queryProxy.runByRouteView(viewData!)
|
|
||||||
const response = res.response;
|
|
||||||
console.log('response', response, viewData);
|
|
||||||
const list = response.data?.list
|
|
||||||
if (!list) {
|
|
||||||
setIsList(false);
|
|
||||||
setObj(response.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isList === false) {
|
|
||||||
setIsList(true);
|
|
||||||
}
|
|
||||||
setData(response.data.list)
|
|
||||||
console.log('res', res);
|
|
||||||
const [_, firstItem] = response.data.list || []
|
|
||||||
if (firstItem) {
|
|
||||||
const cols: ColumnDef<any>[] = Object.keys(firstItem).map(key => ({
|
|
||||||
accessorKey: key,
|
|
||||||
header: key.toUpperCase(),
|
|
||||||
cell: info => info.getValue() + '',
|
|
||||||
}))
|
|
||||||
setColumns(cols)
|
|
||||||
}
|
|
||||||
toast.success('数据获取成功')
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
if (viewData) {
|
|
||||||
setViewData({ ...viewData, response: undefined }) // 触发刷新
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleShowDetails = () => {
|
|
||||||
console.log('Show details for row:', props.viewData)
|
|
||||||
const data = cloneDeep(props.viewData)
|
|
||||||
delete data.api?.proxy;
|
|
||||||
delete data.context?.router;
|
|
||||||
delete data.worker?.worker;
|
|
||||||
const str = JSON.stringify(data, null, 2)
|
|
||||||
toast.info(<pre className='max-h-96 overflow-auto'>{str}</pre>, {
|
|
||||||
autoClose: 5000,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
icon: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
if (selectedRow) {
|
|
||||||
console.log('Edit row:', selectedRow)
|
|
||||||
// 在这里添加编辑逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
if (selectedRow) {
|
|
||||||
console.log('Delete row:', selectedRow)
|
|
||||||
// 在这里添加删除逻辑
|
|
||||||
}
|
|
||||||
studioStore.deleteMessage(props.viewData!)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExport = () => {
|
|
||||||
if (!viewData) return
|
|
||||||
console.log('Export viewData:', viewData)
|
|
||||||
}
|
|
||||||
const handleExportCode = () => {
|
|
||||||
if (!viewData) return
|
|
||||||
console.log('Export code for viewData:', viewData)
|
|
||||||
}
|
|
||||||
const handleSave = () => {
|
|
||||||
if (selectedRow) {
|
|
||||||
console.log('Save row:', selectedRow)
|
|
||||||
toast.success('保存成功')
|
|
||||||
// 在这里添加保存逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveAndOpen = () => {
|
|
||||||
if (selectedRow) {
|
|
||||||
console.log('Save and open row:', selectedRow)
|
|
||||||
toast.success('保存并打开成功')
|
|
||||||
// 在这里添加保存并打开逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
|
||||||
if (viewData) {
|
|
||||||
main()
|
|
||||||
}
|
|
||||||
}, [viewData])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
props.viewData && setViewData(props.viewData as RouterViewItem)
|
|
||||||
}, [])
|
|
||||||
const RenderTable = () => {
|
|
||||||
if (!isList) {
|
|
||||||
return <pre className='bg-gray-100 p-4 rounded-lg overflow-auto'>
|
|
||||||
{JSON.stringify(obj, null, 2)}
|
|
||||||
</pre>
|
|
||||||
}
|
|
||||||
return <table className='w-full border-collapse min-w-max md:w-full'>
|
|
||||||
<thead className='bg-gray-100 border-b-2 border-gray-300 sticky top-0'>
|
|
||||||
{table.getHeaderGroups().map(headerGroup => (
|
|
||||||
<tr key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map(header => (
|
|
||||||
<th
|
|
||||||
key={header.id}
|
|
||||||
className='px-4 py-3 text-left text-sm font-semibold text-gray-700 whitespace-nowrap'
|
|
||||||
>
|
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{table.getRowModel().rows.map((row, idx) => (
|
|
||||||
<tr
|
|
||||||
key={row.id}
|
|
||||||
className={`border-b border-gray-200 transition-colors duration-200 ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'
|
|
||||||
} hover:bg-blue-50`}
|
|
||||||
>
|
|
||||||
{row.getVisibleCells().map(cell => (
|
|
||||||
<td
|
|
||||||
key={cell.id}
|
|
||||||
className='px-4 py-3 text-sm text-gray-600'
|
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
const isPage = type === 'page'
|
|
||||||
return <div id='route-view' className={`w-full ${type === 'component' ? 'max-h-[600px] overflow-y-auto' : 'h-full overflow-auto'} p-4`}>
|
|
||||||
<div className='mb-4'>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<h2 className={`font-bold ${type === 'component' ? 'text-lg' : 'text-2xl'} truncate`} title={`路由视图 - ${viewData?.title || '未命名'}`}>路由视图 - {viewData?.title || '未命名'}</h2>
|
|
||||||
<div className='flex items-center gap-2 relative'>
|
|
||||||
<button
|
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={isLoading}
|
|
||||||
className='p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50'
|
|
||||||
title='刷新'
|
|
||||||
>
|
|
||||||
<RefreshCw size={20} className={isLoading ? 'animate-spin' : ''} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<DropdownMenu open={showMoreMenu} onOpenChange={setShowMoreMenu}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
className='p-2 hover:bg-gray-200 rounded-lg transition-colors'
|
|
||||||
title='更多选项'
|
|
||||||
>
|
|
||||||
<MoreVertical size={20} />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align='end' className='w-48 border-gray-300'>
|
|
||||||
{!isPage && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem onClick={handleSave}>
|
|
||||||
<Save size={16} className='mr-2' />
|
|
||||||
<span>保存</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isPage && (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem onClick={handleSaveAndOpen}>
|
|
||||||
<ExternalLink size={16} className='mr-2' />
|
|
||||||
<span>保存并打开</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem onClick={() => handleShowDetails()}>
|
|
||||||
<Info size={16} className='mr-2' />
|
|
||||||
<span>详情</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={handleEdit}>
|
|
||||||
<Edit size={16} className='mr-2' />
|
|
||||||
<span>编辑</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={handleDelete} variant='destructive'>
|
|
||||||
<Trash2 size={16} className='mr-2' />
|
|
||||||
<span>{!isPage ? '移除' : '删除'}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={handleExport}>
|
|
||||||
<Download size={16} className='mr-2' />
|
|
||||||
<span>导出</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={handleExportCode}>
|
|
||||||
<Code size={16} className='mr-2' />
|
|
||||||
<span>导出代码</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='w-full overflow-x-auto rounded-lg shadow-md border border-gray-300'>
|
|
||||||
<RenderTable />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
export const QueryViewMessages = (props: Props) => {
|
|
||||||
const studioStore = useStudioStore(useShallow((state) => ({
|
|
||||||
messages: state.messages,
|
|
||||||
setMessages: state.setMessages
|
|
||||||
})))
|
|
||||||
const initPage = async () => {
|
|
||||||
const url = new URL(window.location.href)
|
|
||||||
const id = url.searchParams.get('id') || ''
|
|
||||||
if (!id) {
|
|
||||||
toast.error('缺少查询视图ID参数')
|
|
||||||
// return
|
|
||||||
}
|
|
||||||
// 查询query-view的保存的id,赋值后然后执行查询
|
|
||||||
// @ts-ignore
|
|
||||||
const DemoRouterView: RouterViewItem = {
|
|
||||||
id: 'getData',
|
|
||||||
description: '获取数据',
|
|
||||||
title: '获取数据',
|
|
||||||
type: 'api',
|
|
||||||
api: {
|
|
||||||
// url: "/api/router",
|
|
||||||
url: "http://localhost:52000/api/router",
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
path: 'router',
|
|
||||||
key: 'list'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
studioStore.setMessages([DemoRouterView])
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
|
||||||
const type = props.type || 'page'
|
|
||||||
if (type === 'page') {
|
|
||||||
initPage()
|
|
||||||
}
|
|
||||||
}, [props.type])
|
|
||||||
return <div>
|
|
||||||
{studioStore.messages.map((msg, index) => (
|
|
||||||
<div key={msg._id || msg.id} className="p-4 border-b border-gray-200">
|
|
||||||
<QueryView viewData={msg} type={props.type} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
export const AppProvider = () => {
|
|
||||||
return <main className='w-full h-screen flex flex-col overflow-auto'>
|
|
||||||
<QueryViewMessages type="page" />
|
|
||||||
<ToastContainer position="top-center" autoClose={1000} hideProgressBar={false} newestOnTop={false} closeOnClick rtl={false} pauseOnFocusLoss draggable pauseOnHover transition={Slide} />
|
|
||||||
</main>
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { toast, ToastContainer, Slide } from 'react-toastify';
|
|
||||||
|
|
||||||
export const AppProvider = () => {
|
|
||||||
return <main className='w-full'>
|
|
||||||
<App />
|
|
||||||
<ToastContainer
|
|
||||||
position="top-right"
|
|
||||||
autoClose={3000}
|
|
||||||
hideProgressBar
|
|
||||||
newestOnTop
|
|
||||||
closeOnClick
|
|
||||||
rtl={false}
|
|
||||||
pauseOnFocusLoss
|
|
||||||
draggable
|
|
||||||
pauseOnHover
|
|
||||||
theme="light"
|
|
||||||
transition={Slide} />
|
|
||||||
</main>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const App = () => {
|
|
||||||
return (
|
|
||||||
<div>Studio App</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
import { toast, ToastContainer, Slide } from 'react-toastify';
|
|
||||||
import { useStudioStore } from './store.js';
|
|
||||||
import { use, useEffect, useState } from 'react';
|
|
||||||
import { MonitorPlay, Play, PanelLeft, PanelLeftClose, PanelRight, PanelRightClose, Filter, FilterX, Search, X } from 'lucide-react';
|
|
||||||
import { Panel, Group } from 'react-resizable-panels'
|
|
||||||
import { ViewList } from '../view/list.js';
|
|
||||||
import { useShallow } from 'zustand/shallow';
|
|
||||||
import { Chat } from '../chat/index.js';
|
|
||||||
import { Input } from '@/components/ui/input.tsx';
|
|
||||||
export const AppProvider = () => {
|
|
||||||
const { showLeftPanel, showRightPanel } = useStudioStore(useShallow((state) => ({
|
|
||||||
showLeftPanel: state.showLeftPanel,
|
|
||||||
showRightPanel: state.showRightPanel,
|
|
||||||
})));
|
|
||||||
return <main className='w-full h-screen flex flex-col overflow-hidden'>
|
|
||||||
<Group className="h-full flex-1 overflow-hidden">
|
|
||||||
{showLeftPanel && <Panel defaultSize={300} minSize={250} maxSize={500} className="border-r border-gray-300 overflow-auto">
|
|
||||||
<ViewList />
|
|
||||||
</Panel>}
|
|
||||||
<Panel>
|
|
||||||
<WrapperHeader>
|
|
||||||
<Group className="h-full overflow-hidden">
|
|
||||||
<Panel >
|
|
||||||
<App />
|
|
||||||
</Panel>
|
|
||||||
{showRightPanel && <Panel defaultSize={500} minSize={300} maxSize={600} className="border-l border-gray-300 overflow-auto">
|
|
||||||
<Chat />
|
|
||||||
</Panel>}
|
|
||||||
</Group>
|
|
||||||
</WrapperHeader>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
</Group>
|
|
||||||
<ToastContainer
|
|
||||||
position="top-right"
|
|
||||||
autoClose={3000}
|
|
||||||
hideProgressBar
|
|
||||||
newestOnTop
|
|
||||||
closeOnClick
|
|
||||||
rtl={false}
|
|
||||||
pauseOnFocusLoss
|
|
||||||
draggable
|
|
||||||
pauseOnHover
|
|
||||||
theme="light"
|
|
||||||
transition={Slide} />
|
|
||||||
</main>
|
|
||||||
}
|
|
||||||
export const WrapperHeader = (props: { children: React.ReactNode }) => {
|
|
||||||
const showLeftPanel = useStudioStore(state => state.showLeftPanel);
|
|
||||||
const store = useStudioStore(useShallow((state) => ({
|
|
||||||
showLeftPanel: state.showLeftPanel,
|
|
||||||
setShowLeftPanel: state.setShowLeftPanel,
|
|
||||||
showFilter: state.showFilter,
|
|
||||||
setShowFilter: state.setShowFilter,
|
|
||||||
showRightPanel: state.showRightPanel,
|
|
||||||
setShowRightPanel: state.setShowRightPanel,
|
|
||||||
})));
|
|
||||||
return <div className='h-full'>
|
|
||||||
<div className="w-full h-12 flex items-center justify-between px-4 border-b border-gray-200 bg-white">
|
|
||||||
<div className="cursor-pointer text-gray-600 hover:text-gray-900 transition-colors" title="Kevisual Router Studio" onClick={() => {
|
|
||||||
store.setShowLeftPanel(!store.showLeftPanel);
|
|
||||||
}}>
|
|
||||||
{showLeftPanel ? <PanelLeftClose size={16} /> : <PanelLeft size={16} />}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
className="p-1.5 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
|
|
||||||
title={store.showFilter ? "隐藏过滤器" : "显示过滤器"}
|
|
||||||
onClick={() => store.setShowFilter(!store.showFilter)}
|
|
||||||
>
|
|
||||||
{store.showFilter ? <FilterX size={16} /> : <Filter size={16} />}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-1.5 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
|
|
||||||
title={store.showRightPanel ? "隐藏右侧面板" : "显示右侧面板"}
|
|
||||||
onClick={() => store.setShowRightPanel(!store.showRightPanel)}
|
|
||||||
>
|
|
||||||
{store.showRightPanel ? <PanelRightClose size={16} /> : <PanelRight size={16} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ height: 'calc(100% - 3rem)' }} className="overflow-auto">
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</div >
|
|
||||||
}
|
|
||||||
interface RouteItem {
|
|
||||||
id: string;
|
|
||||||
path?: string;
|
|
||||||
key?: string;
|
|
||||||
description?: string;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const App = () => {
|
|
||||||
const { routes, queryRouteList, run, loading, searchRoutes, ...store } = useStudioStore(useShallow((state) => ({
|
|
||||||
routes: state.routes,
|
|
||||||
searchRoutes: state.searchRoutes,
|
|
||||||
queryRouteList: state.queryRouteList,
|
|
||||||
run: state.run,
|
|
||||||
loading: state.loading,
|
|
||||||
showFilter: state.showFilter,
|
|
||||||
currentView: state.currentView,
|
|
||||||
setShowFilter: state.setShowFilter,
|
|
||||||
})));
|
|
||||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
|
||||||
const [visibleIds, setVisibleIds] = useState<Set<string>>(new Set());
|
|
||||||
const [searchKeyword, setSearchKeyword] = useState<string>('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
queryRouteList();
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
if (store.showFilter) {
|
|
||||||
// 当显示过滤器时,自动聚焦输入框
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
const input = document.querySelector('input[placeholder="过滤路由..."]') as HTMLInputElement;
|
|
||||||
if (input) {
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
if (!store.currentView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const viewId = store.currentView.viewId;
|
|
||||||
const viewItem = store.currentView.views.find(v => v.id === viewId);
|
|
||||||
if (viewItem && viewItem.query) {
|
|
||||||
setSearchKeyword(viewItem.query);
|
|
||||||
}
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [store.showFilter, store.currentView?.viewId]);
|
|
||||||
const handleSearch = async (keyword: string) => {
|
|
||||||
if (keyword.trim()) {
|
|
||||||
await searchRoutes(keyword);
|
|
||||||
} else {
|
|
||||||
await queryRouteList();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
await handleSearch(searchKeyword);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClear = async () => {
|
|
||||||
setSearchKeyword('');
|
|
||||||
await queryRouteList();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDescription = (id: string) => {
|
|
||||||
const newExpanded = new Set(expandedIds);
|
|
||||||
if (newExpanded.has(id)) {
|
|
||||||
newExpanded.delete(id);
|
|
||||||
} else {
|
|
||||||
newExpanded.add(id);
|
|
||||||
}
|
|
||||||
setExpandedIds(newExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleIdVisibility = (e: React.MouseEvent, id: string) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const newVisible = new Set(visibleIds);
|
|
||||||
if (newVisible.has(id)) {
|
|
||||||
newVisible.delete(id);
|
|
||||||
} else {
|
|
||||||
newVisible.add(id);
|
|
||||||
}
|
|
||||||
setVisibleIds(newVisible);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-5xl mx-auto p-6 h-full overflow-auto">
|
|
||||||
{loading && <div className="text-center text-gray-500 mb-4">加载中...</div>}
|
|
||||||
{store.showFilter && (
|
|
||||||
<div className="mb-3 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md border border-gray-300 bg-white focus-within:ring-2 focus-within:ring-gray-400 focus-within:ring-offset-1 focus-within:border-gray-400">
|
|
||||||
<Search size={16} className="text-gray-700 flex-shrink-0" strokeWidth={2} />
|
|
||||||
<Input
|
|
||||||
placeholder="输入路由关键词进行搜索..."
|
|
||||||
className="w-full !border-0 !shadow-none !outline-none bg-transparent focus-visible:!outline-none focus-visible:!ring-0 focus-visible:!ring-offset-0 text-sm text-gray-900 placeholder:text-gray-500"
|
|
||||||
value={searchKeyword}
|
|
||||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
{searchKeyword && (
|
|
||||||
<button
|
|
||||||
onClick={handleClear}
|
|
||||||
className="p-1 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-200 active:bg-gray-300 transition-all duration-150 cursor-pointer flex-shrink-0"
|
|
||||||
title="清空搜索"
|
|
||||||
>
|
|
||||||
<X size={16} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={`space-y-1 ${loading ? "opacity-50 pointer-events-none" : ""}`}>
|
|
||||||
{routes.map((route: RouteItem) => {
|
|
||||||
const isExpanded = expandedIds.has(route.id);
|
|
||||||
const isIdVisible = visibleIds.has(route.id);
|
|
||||||
const len = route.description?.length || 0;
|
|
||||||
const isLongDescription = len > 20;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={route.id}
|
|
||||||
className="px-4 py-3 border-b border-gray-100 hover:bg-gray-50/50 transition-all duration-200 animate-in fade-in slide-in-from-top-1 duration-400"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2.5">
|
|
||||||
{/* ID and Path/Key in one line */}
|
|
||||||
<div className="flex gap-2.5 flex-wrap items-center justify-between">
|
|
||||||
<div className="flex gap-2.5 flex-wrap items-center">
|
|
||||||
<span
|
|
||||||
onClick={(e) => toggleIdVisibility(e, route.id)}
|
|
||||||
className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold bg-gray-900 text-white cursor-pointer hover:bg-gray-700 transition-all duration-200 shadow-sm"
|
|
||||||
>
|
|
||||||
{isIdVisible ? route.id : 'id'}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{(route.path || route.key) && (
|
|
||||||
<div className="bg-gray-100 px-3 py-1.5 rounded-md font-mono text-sm text-gray-900 border border-gray-200">
|
|
||||||
{route.path}{route.key && ` / ${route.key}`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='inline-flex items-center justify-center gap-1'>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="p-1.5 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
|
|
||||||
title="直接运行"
|
|
||||||
onClick={() => run(route)}
|
|
||||||
>
|
|
||||||
<Play size={14} strokeWidth={2.5} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button className="p-1.5 rounded-md text-gray-20 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
|
|
||||||
title="高级运行"
|
|
||||||
onClick={() => run(route)}>
|
|
||||||
<MonitorPlay size={14} strokeWidth={2.5} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description with expand/collapse */}
|
|
||||||
{route.description && (
|
|
||||||
<div
|
|
||||||
className="cursor-pointer group"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`text-gray-700 transition-colors duration-200 cursor-pointer overflow-hidden`}
|
|
||||||
>
|
|
||||||
{isExpanded ? (
|
|
||||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">
|
|
||||||
{route.description}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm leading-relaxed overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
{route.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isLongDescription && (
|
|
||||||
<p className="text-xs text-gray-400 mt-1 group-hover:text-gray-500 transition-colors duration-200"
|
|
||||||
onClick={() => toggleDescription(route.id)}
|
|
||||||
>
|
|
||||||
{isExpanded ? '点击收起' : '点击展开'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Metadata */}
|
|
||||||
{route.metadata && Object.keys(route.metadata).length > 0 && (
|
|
||||||
<div className="mt-0.5">
|
|
||||||
<span className="text-xs text-gray-500 mr-2 font-medium">Metadata:</span>
|
|
||||||
<div className="flex gap-1.5 flex-wrap">
|
|
||||||
{Object.entries(route.metadata).map(([k, v]) => (
|
|
||||||
<span
|
|
||||||
key={k}
|
|
||||||
className="inline-flex items-center px-2 py-1 rounded text-xs bg-white text-gray-700 border border-gray-200 shadow-sm"
|
|
||||||
>
|
|
||||||
<span className="font-semibold text-gray-900">{k}:</span> {String(v)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
import { QueryProxy, RouterViewData, RouterViewItem, pickRouterViewData } from '@kevisual/api'
|
|
||||||
import { query } from '@/modules/query.ts'
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { use } from '@kevisual/context'
|
|
||||||
import { MyCache } from '@kevisual/cache'
|
|
||||||
import { persist } from 'zustand/middleware';
|
|
||||||
import { app } from '@/index.ts'
|
|
||||||
import { cloneDeep, random } from 'es-toolkit'
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
const historyReplace = (url: string) => {
|
|
||||||
if (window.history.replaceState) {
|
|
||||||
window.history.replaceState(null, '', url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type RouteItem = {
|
|
||||||
id: string;
|
|
||||||
path?: string;
|
|
||||||
key?: string;
|
|
||||||
description?: string;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type RouteViewList = Array<RouterViewData>;
|
|
||||||
|
|
||||||
|
|
||||||
interface StudioState {
|
|
||||||
loading: boolean;
|
|
||||||
setLoading: (loading: boolean) => void;
|
|
||||||
routes: Array<RouteItem>;
|
|
||||||
searchRoutes: (keyword: string) => Promise<void>;
|
|
||||||
run: (route: RouteItem) => Promise<void>;
|
|
||||||
queryProxy?: QueryProxy;
|
|
||||||
init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>;
|
|
||||||
routeViewList: RouteViewList;
|
|
||||||
getViewList: () => Promise<void>;
|
|
||||||
queryRouteList: () => Promise<void>;
|
|
||||||
getCurrentView: () => Promise<void>;
|
|
||||||
updateRouteView: (view: RouterViewData) => Promise<void>;
|
|
||||||
deleteRouteView: (id: string) => Promise<void>;
|
|
||||||
deleteRouteViewItem: (id: string, viewId: string) => void;
|
|
||||||
currentView?: RouterViewData;
|
|
||||||
setCurrentView: (view?: RouterViewData) => Promise<void>;
|
|
||||||
showLeftPanel: boolean;
|
|
||||||
setShowLeftPanel: (show: boolean) => void;
|
|
||||||
showFilter: boolean;
|
|
||||||
setShowFilter: (show: boolean) => void;
|
|
||||||
showRightPanel: boolean;
|
|
||||||
setShowRightPanel: (show: boolean) => void;
|
|
||||||
messages: any[];
|
|
||||||
setMessages: (messages: any[]) => void;
|
|
||||||
addMessage: (message: any) => void;
|
|
||||||
deleteMessage: (message: any) => void;
|
|
||||||
}
|
|
||||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
export const useStudioStore = create<StudioState>()(
|
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
loading: false,
|
|
||||||
setLoading: (loading: boolean) => set({ loading }),
|
|
||||||
routes: [],
|
|
||||||
searchRoutes: async (keyword: string) => {
|
|
||||||
const state = await get().init();
|
|
||||||
let queryProxy = state.queryProxy;
|
|
||||||
const routes: any[] = await queryProxy.listRoutes(() => true, { query: keyword });
|
|
||||||
set({ routes });
|
|
||||||
},
|
|
||||||
currentView: undefined,
|
|
||||||
queryRouteList: async () => {
|
|
||||||
await get().getCurrentView();
|
|
||||||
const state = await get().init();
|
|
||||||
let currentView: RouterViewData | undefined = get().currentView;
|
|
||||||
let queryProxy = state.queryProxy;
|
|
||||||
const viewId = currentView?.viewId ?? ''
|
|
||||||
const routes: any[] = await queryProxy.listRoutes(() => true, { viewId });
|
|
||||||
set({ routes });
|
|
||||||
},
|
|
||||||
setCurrentView: async (view?: RouterViewData) => {
|
|
||||||
const beforeView = get().currentView;
|
|
||||||
set({ currentView: view, routes: [] });
|
|
||||||
const viewId = view?.viewId || '';
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
if (viewId) {
|
|
||||||
url.searchParams.set('viewId', viewId);
|
|
||||||
} else {
|
|
||||||
url.searchParams.delete('viewId');
|
|
||||||
}
|
|
||||||
historyReplace(url.toString());
|
|
||||||
console.log('视图切换', beforeView, view);
|
|
||||||
await get().init(beforeView?.id !== view?.id);
|
|
||||||
await get().queryRouteList();
|
|
||||||
},
|
|
||||||
getViewList: async () => {
|
|
||||||
const res = await query.post({ path: 'views', key: 'list' });
|
|
||||||
if (res.code === 200) {
|
|
||||||
const list = res.data.list as RouteViewList || [];
|
|
||||||
set({ routeViewList: list });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCurrentView: async () => {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const viewId = url.searchParams.get('viewId');
|
|
||||||
if (!viewId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await query.post({ path: 'views', key: 'current', data: { viewId: viewId } });
|
|
||||||
if (res.code === 200) {
|
|
||||||
const view = res.data as RouterViewData;
|
|
||||||
view.viewId = viewId;
|
|
||||||
set({ currentView: view });
|
|
||||||
} else {
|
|
||||||
set({ currentView: undefined });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
routeViewList: [],
|
|
||||||
updateRouteView: async (view: RouterViewData) => {
|
|
||||||
const res = await query.post({ path: 'views', key: 'update', data: view });
|
|
||||||
if (res.code !== 200) {
|
|
||||||
toast.error(`视图更新失败:${res.message || '未知错误'}`);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
get().getViewList();
|
|
||||||
toast.success('视图更新成功');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteRouteView: async (id: string) => {
|
|
||||||
const res = await query.post({ path: 'views', key: 'delete', data: { id } });
|
|
||||||
if (res.code !== 200) {
|
|
||||||
toast.error(`视图删除失败:${res.message || '未知错误'}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
get().getViewList();
|
|
||||||
toast.success('视图删除成功');
|
|
||||||
},
|
|
||||||
deleteRouteViewItem: (id: string, viewId: string) => {
|
|
||||||
const routeViewList = get().routeViewList;
|
|
||||||
const viewItem = routeViewList.find(view => view.id === id);
|
|
||||||
if (!viewItem) {
|
|
||||||
toast.error('视图项不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
viewItem.views = viewItem.views?.filter(v => v.id !== viewId);
|
|
||||||
get().updateRouteView(viewItem);
|
|
||||||
const newList = routeViewList.map(view => {
|
|
||||||
if (view.id === viewItem.id) {
|
|
||||||
return viewItem;
|
|
||||||
}
|
|
||||||
return view;
|
|
||||||
});
|
|
||||||
set({ routeViewList: [...newList] });
|
|
||||||
console.log('删除视图项', id, newList);
|
|
||||||
},
|
|
||||||
run: async (route: RouteItem) => {
|
|
||||||
const state = await get().init();
|
|
||||||
let queryProxy = state.queryProxy;
|
|
||||||
const showRightPanel = get().showRightPanel;
|
|
||||||
const action = {
|
|
||||||
path: route.path,
|
|
||||||
key: route.key
|
|
||||||
}
|
|
||||||
const res = await queryProxy.run(action);
|
|
||||||
if (res.code !== 200) {
|
|
||||||
toast.error(`运行失败:${res.message || '未知错误'}`);
|
|
||||||
} else if (res.code === 200) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
if (showRightPanel) {
|
|
||||||
if (route.metadata && route.metadata?.viewItem) {
|
|
||||||
const messages = get().messages
|
|
||||||
const viewItem = cloneDeep(route.metadata.viewItem) as RouterViewItem;
|
|
||||||
viewItem.response = res;
|
|
||||||
viewItem.action = action
|
|
||||||
viewItem.description = route.description || viewItem.description;
|
|
||||||
// @ts-ignore
|
|
||||||
viewItem._id = nanoid(16);
|
|
||||||
set({ messages: [...messages, viewItem] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
queryProxy: undefined,
|
|
||||||
router: undefined,
|
|
||||||
init: async (force?: boolean) => {
|
|
||||||
// let _url = 'http://localhost:52002/api/router'; // Github starred
|
|
||||||
// let _url = 'http://localhost:52000/api/router'; // 浏览器
|
|
||||||
// let _url = '/api/router';
|
|
||||||
let queryProxy = get().queryProxy;
|
|
||||||
if (queryProxy && !force) {
|
|
||||||
return { queryProxy };
|
|
||||||
}
|
|
||||||
let currentView: RouterViewData | undefined = get().currentView;
|
|
||||||
// @ts-ignore
|
|
||||||
const routerViewData: RouterViewData = currentView || {
|
|
||||||
_id: nanoid(16),
|
|
||||||
views: [{
|
|
||||||
id: '',
|
|
||||||
title: '默认视图',
|
|
||||||
query: ``
|
|
||||||
}],
|
|
||||||
data: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: '默认路由',
|
|
||||||
id: '',
|
|
||||||
description: '',
|
|
||||||
type: 'api',
|
|
||||||
api: {
|
|
||||||
url: '/api/router'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
viewId: '',
|
|
||||||
}
|
|
||||||
console.log('初始化 QueryProxy', routerViewData);
|
|
||||||
queryProxy = new QueryProxy({
|
|
||||||
routerViewData,
|
|
||||||
router: app as any,
|
|
||||||
});
|
|
||||||
console.log('初始化 QueryProxy 完成', queryProxy.token);
|
|
||||||
set({ loading: true });
|
|
||||||
await sleep(1000); // 保证 loading 状态更新
|
|
||||||
await queryProxy.init();
|
|
||||||
set({ loading: false });
|
|
||||||
set({ queryProxy });
|
|
||||||
return { queryProxy }
|
|
||||||
},
|
|
||||||
showLeftPanel: false,
|
|
||||||
setShowLeftPanel: (show: boolean) => set({ showLeftPanel: show }),
|
|
||||||
showRightPanel: true,
|
|
||||||
setShowRightPanel: (show: boolean) => set({ showRightPanel: show }),
|
|
||||||
showFilter: false,
|
|
||||||
setShowFilter: (show: boolean) => set({ showFilter: show }),
|
|
||||||
messages: [],
|
|
||||||
setMessages: (messages: any[]) => set({ messages }),
|
|
||||||
deleteMessage: (message: any) => {
|
|
||||||
const messages = get().messages;
|
|
||||||
const index = messages.findIndex(m => {
|
|
||||||
return m._id === message._id || m.id === message.id;
|
|
||||||
});
|
|
||||||
if (index !== -1) {
|
|
||||||
messages.splice(index, 1);
|
|
||||||
set({ messages: [...messages] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addMessage: (message: any) => {
|
|
||||||
const messages = get().messages;
|
|
||||||
set({ messages: [...messages, message] });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'studio-storage',
|
|
||||||
partialize: (state) => ({
|
|
||||||
showLeftPanel: state.showLeftPanel,
|
|
||||||
showRightPanel: state.showRightPanel,
|
|
||||||
showFilter: state.showFilter,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
use('studioStore', () => {
|
|
||||||
return useStudioStore;
|
|
||||||
});
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
|
||||||
import { Query } from "@kevisual/query"
|
|
||||||
import { QueryRouterServer } from "@kevisual/router"
|
|
||||||
import { nanoid } from "nanoid"
|
|
||||||
|
|
||||||
export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker;
|
|
||||||
type RouteViewBase = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
export type RouterViewApi = {
|
|
||||||
type: 'api',
|
|
||||||
api: {
|
|
||||||
url: string,
|
|
||||||
// 已初始化的query实例,不需要编辑配置
|
|
||||||
query?: Query
|
|
||||||
}
|
|
||||||
} & RouteViewBase;
|
|
||||||
|
|
||||||
export type RouterViewContext = {
|
|
||||||
type: 'context',
|
|
||||||
context: {
|
|
||||||
key: string,
|
|
||||||
// 从context中获取router,不需要编辑配置
|
|
||||||
router?: QueryRouterServer
|
|
||||||
}
|
|
||||||
} & RouteViewBase;
|
|
||||||
export type RouterViewWorker = {
|
|
||||||
type: 'worker',
|
|
||||||
worker: {
|
|
||||||
type: 'Worker' | 'SharedWorker' | 'serviceWorker',
|
|
||||||
url: string,
|
|
||||||
// 已初始化的worker实例,不需要编辑配置
|
|
||||||
worker?: Worker | SharedWorker | ServiceWorker,
|
|
||||||
/**
|
|
||||||
* worker选项
|
|
||||||
* default: { type: 'module' }
|
|
||||||
*/
|
|
||||||
workerOptions?: {
|
|
||||||
type: 'module' | 'classic'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} & RouteViewBase;
|
|
||||||
interface DataItemFormProps {
|
|
||||||
item: RouterViewItem
|
|
||||||
onChange: (item: any) => void
|
|
||||||
onRemove: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) => {
|
|
||||||
const handleChange = (field: string, value: any) => {
|
|
||||||
if (field === 'type') {
|
|
||||||
const newItem: RouterViewItem = { ...item, type: value }
|
|
||||||
if (value === 'api' && !('api' in item)) {
|
|
||||||
(newItem as RouterViewApi).api = { url: '' }
|
|
||||||
} else if (value === 'context' && !('context' in item)) {
|
|
||||||
(newItem as RouterViewContext).context = { key: '' }
|
|
||||||
} else if (value === 'worker' && !('worker' in item)) {
|
|
||||||
(newItem as RouterViewWorker).worker = { type: 'Worker', url: '', workerOptions: { type: 'module' } }
|
|
||||||
}
|
|
||||||
if (!newItem.id) {
|
|
||||||
newItem.id = nanoid(16)
|
|
||||||
}
|
|
||||||
onChange(newItem)
|
|
||||||
} else {
|
|
||||||
onChange({ ...item, [field]: value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNestedChange = (parent: string, field: string, value: any) => {
|
|
||||||
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
|
|
||||||
const newParentValue: Record<string, any> = {
|
|
||||||
...(parentValue || {}),
|
|
||||||
[field]: value
|
|
||||||
}
|
|
||||||
onChange({ ...item, [parent]: newParentValue })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNestedDeepChange = (parent: string, nestedParent: string, field: string, value: any) => {
|
|
||||||
const parentValue = item[parent as keyof RouterViewItem] as Record<string, any> | undefined
|
|
||||||
const nestedValue = parentValue?.[nestedParent] as Record<string, any> | undefined
|
|
||||||
const newNestedValue: Record<string, any> = {
|
|
||||||
...(nestedValue || {}),
|
|
||||||
[field]: value
|
|
||||||
}
|
|
||||||
const newParentValue: Record<string, any> = {
|
|
||||||
...(parentValue || {}),
|
|
||||||
[nestedParent]: newNestedValue
|
|
||||||
}
|
|
||||||
onChange({ ...item, [parent]: newParentValue })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border border-gray-300 rounded-lg p-4 mb-4 space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h3 className="font-medium">数据项配置</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRemove}
|
|
||||||
className="text-sm text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>标题</Label>
|
|
||||||
<Input
|
|
||||||
value={item.title || ''}
|
|
||||||
onChange={(e) => handleChange('title', e.target.value)}
|
|
||||||
placeholder="输入标题"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>类型</Label>
|
|
||||||
<select
|
|
||||||
value={item.type}
|
|
||||||
onChange={(e) => handleChange('type', e.target.value)}
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<option value="api">API</option>
|
|
||||||
<option value="context">Context</option>
|
|
||||||
<option value="worker">Worker</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="enabled"
|
|
||||||
checked={item.enabled !== false}
|
|
||||||
onCheckedChange={(checked) => handleChange('enabled', checked)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="enabled" className="cursor-pointer">启用</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(item.type === 'api') && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>API URL</Label>
|
|
||||||
<Input
|
|
||||||
value={item.api?.url || ''}
|
|
||||||
onChange={(e) => handleNestedChange('api', 'url', e.target.value)}
|
|
||||||
placeholder="输入 API 地址"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{item.type === 'context' && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Context Key</Label>
|
|
||||||
<Input
|
|
||||||
value={item.context?.key || ''}
|
|
||||||
onChange={(e) => handleNestedChange('context', 'key', e.target.value)}
|
|
||||||
placeholder="输入 Context Key"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{item.type === 'worker' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Worker Type</Label>
|
|
||||||
<select
|
|
||||||
value={item.worker?.type || 'Worker'}
|
|
||||||
onChange={(e) => handleNestedChange('worker', 'type', e.target.value)}
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<option value="Worker">Worker</option>
|
|
||||||
<option value="SharedWorker">SharedWorker</option>
|
|
||||||
<option value="serviceWorker">ServiceWorker</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Worker URL</Label>
|
|
||||||
<Input
|
|
||||||
value={item.worker?.url || ''}
|
|
||||||
onChange={(e) => handleNestedChange('worker', 'url', e.target.value)}
|
|
||||||
placeholder="输入 Worker URL"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Worker Options Type</Label>
|
|
||||||
<select
|
|
||||||
value={item.worker?.workerOptions?.type || 'module'}
|
|
||||||
onChange={(e) => handleNestedDeepChange('worker', 'workerOptions', 'type', e.target.value)}
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<option value="module">Module</option>
|
|
||||||
<option value="classic">Classic</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import { useState, useEffect, useRef } from "react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
||||||
import { DataItemForm } from "@/apps/view/components/DataItemForm"
|
|
||||||
import { ViewFormItem } from "@/apps/view/components/ViewFormItem"
|
|
||||||
import { nanoid } from "nanoid"
|
|
||||||
|
|
||||||
interface ViewEditorProps {
|
|
||||||
open: boolean
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
data?: {
|
|
||||||
id?: string
|
|
||||||
title?: string
|
|
||||||
data?: { items: any[] }
|
|
||||||
views?: any[]
|
|
||||||
}
|
|
||||||
onSave: (data: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps) => {
|
|
||||||
const [title, setTitle] = useState('')
|
|
||||||
const [dataItems, setDataItems] = useState<any[]>([])
|
|
||||||
const [views, setViews] = useState<any[]>([])
|
|
||||||
const dataItemsScrollRef = useRef<HTMLDivElement>(null)
|
|
||||||
const viewsScrollRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const isUpdate = !!data?.id
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setTitle(data?.title || '')
|
|
||||||
setDataItems(data?.data?.items || [])
|
|
||||||
setViews(data?.views || [])
|
|
||||||
}
|
|
||||||
}, [open, data])
|
|
||||||
|
|
||||||
const handleAddDataItem = () => {
|
|
||||||
setDataItems([...dataItems, { type: 'api', api: { url: '' } }])
|
|
||||||
// 异步滚动到底部,使用 smooth 平滑滚动
|
|
||||||
setTimeout(() => {
|
|
||||||
if (dataItemsScrollRef.current) {
|
|
||||||
dataItemsScrollRef.current.scrollTo({
|
|
||||||
top: dataItemsScrollRef.current.scrollHeight,
|
|
||||||
behavior: 'smooth'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateDataItem = (index: number, item: any) => {
|
|
||||||
const newItems = [...dataItems]
|
|
||||||
newItems[index] = item
|
|
||||||
setDataItems(newItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveDataItem = (index: number) => {
|
|
||||||
setDataItems(dataItems.filter((_, i) => i !== index))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddView = () => {
|
|
||||||
setViews([...views, { id: nanoid(16), title: '', query: '' }])
|
|
||||||
// 异步滚动到底部,使用 smooth 平滑滚动
|
|
||||||
setTimeout(() => {
|
|
||||||
if (viewsScrollRef.current) {
|
|
||||||
viewsScrollRef.current.scrollTo({
|
|
||||||
top: viewsScrollRef.current.scrollHeight,
|
|
||||||
behavior: 'smooth'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateView = (index: number, view: any) => {
|
|
||||||
const newViews = [...views]
|
|
||||||
newViews[index] = view
|
|
||||||
setViews(newViews)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveView = (index: number) => {
|
|
||||||
setViews(views.filter((_, i) => i !== index))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const pickData = dataItems.map(item => {
|
|
||||||
if (item.type === 'api') {
|
|
||||||
delete item.api.query
|
|
||||||
}
|
|
||||||
if (item.type === 'worker') {
|
|
||||||
delete item.worker.worker
|
|
||||||
}
|
|
||||||
if (item.type === 'context') {
|
|
||||||
delete item.context.router
|
|
||||||
}
|
|
||||||
if (item.type === 'page') {
|
|
||||||
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
const viewData = {
|
|
||||||
id: data?.id,
|
|
||||||
title,
|
|
||||||
data: {
|
|
||||||
items: pickData
|
|
||||||
},
|
|
||||||
views
|
|
||||||
}
|
|
||||||
onSave(viewData)
|
|
||||||
onOpenChange(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onOpenChange(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="max-w-3xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{isUpdate ? '编辑视图' : '新增视图'}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* 固定的视图标题 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="title">视图标题</Label>
|
|
||||||
<Input
|
|
||||||
id="title"
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
|
||||||
placeholder="输入视图标题"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs 容器 */}
|
|
||||||
<Tabs defaultValue="data" className="w-full flex flex-col">
|
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
|
||||||
<TabsTrigger value="data">数据项配置</TabsTrigger>
|
|
||||||
<TabsTrigger value="views">视图配置</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* 数据项配置 Tab */}
|
|
||||||
<TabsContent value="data" className="flex flex-col mt-4">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h3 className="font-medium">数据项配置 (data.items)</h3>
|
|
||||||
<Button type="button" variant="outline" size="sm" onClick={handleAddDataItem} className="cursor-pointer">
|
|
||||||
添加数据项
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div ref={dataItemsScrollRef} className="space-y-4 max-h-[50vh] overflow-y-auto pr-4">
|
|
||||||
{dataItems.length === 0 ? (
|
|
||||||
<div className="text-center text-sm text-gray-500 py-8">
|
|
||||||
暂无数据项,点击"添加数据项"开始配置
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
dataItems.map((item, index) => (
|
|
||||||
<DataItemForm
|
|
||||||
key={index}
|
|
||||||
item={item}
|
|
||||||
onChange={(newItem) => handleUpdateDataItem(index, newItem)}
|
|
||||||
onRemove={() => handleRemoveDataItem(index)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* 视图配置 Tab */}
|
|
||||||
<TabsContent value="views" className="flex flex-col mt-4">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h3 className="font-medium">视图配置 (views)</h3>
|
|
||||||
<Button type="button" variant="outline" size="sm" onClick={handleAddView} className="cursor-pointer">
|
|
||||||
添加视图
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div ref={viewsScrollRef} className="space-y-4 max-h-[50vh] overflow-y-auto pr-4">
|
|
||||||
{views.length === 0 ? (
|
|
||||||
<div className="text-center text-sm text-gray-500 py-8">
|
|
||||||
暂无视图,点击"添加视图"开始配置
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
views.map((view, index) => (
|
|
||||||
<ViewFormItem
|
|
||||||
key={view.id || index}
|
|
||||||
view={view}
|
|
||||||
onChange={(newView) => handleUpdateView(index, newView)}
|
|
||||||
onRemove={() => handleRemoveView(index)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" onClick={handleClose} className="cursor-pointer">
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button type="button" onClick={handleSave} className="cursor-pointer">
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { toast } from "react-toastify"
|
|
||||||
|
|
||||||
interface ViewFormProps {
|
|
||||||
view: any
|
|
||||||
onChange: (view: any) => void
|
|
||||||
onRemove: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ViewFormItem = ({ view, onChange, onRemove }: ViewFormProps) => {
|
|
||||||
const handleChange = (field: string, value: any) => {
|
|
||||||
onChange({ ...view, [field]: value })
|
|
||||||
}
|
|
||||||
const handleCopyId = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(id);
|
|
||||||
toast.success('已复制到剪贴板', { autoClose: 1000 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('复制失败', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="border border-gray-300 rounded-lg p-4 mb-4 space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h3 className="font-medium">视图配置</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRemove}
|
|
||||||
className="text-sm text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2" onClick={() => handleCopyId(view.id)}>
|
|
||||||
<Label>ID</Label>
|
|
||||||
<Input
|
|
||||||
value={view.id || ''}
|
|
||||||
onChange={(e) => handleChange('id', e.target.value)}
|
|
||||||
placeholder="自动生成"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>标题</Label>
|
|
||||||
<Input
|
|
||||||
value={view.title || ''}
|
|
||||||
onChange={(e) => handleChange('title', e.target.value)}
|
|
||||||
placeholder="输入视图标题"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>查询语句</Label>
|
|
||||||
<Input
|
|
||||||
value={view.query || ''}
|
|
||||||
onChange={(e) => handleChange('query', e.target.value)}
|
|
||||||
placeholder="输入查询语句"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useStudioStore } from '../studio/store.js';
|
|
||||||
import { Search, RotateCw, Plus, MoreHorizontal, Layout, Edit2, Trash2 } from "lucide-react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { ViewEditor } from "@/apps/view/components/ViewEditor.tsx";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
const ViewItem = ({ view, onEdit, onDelete, onDeleteViewItem }: { view: any; onEdit: (view: any) => void; onDelete: (id: string) => void; onDeleteViewItem: (id: string, viewId: string) => void }) => {
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
const studioStore = useStudioStore();
|
|
||||||
useEffect(() => {
|
|
||||||
const currentViewId = studioStore.currentView?.viewId;
|
|
||||||
if (view.views.some((v: any) => v.id === currentViewId)) {
|
|
||||||
setExpanded(true);
|
|
||||||
}
|
|
||||||
}, [studioStore.currentView?.viewId]);
|
|
||||||
const ShowViews = (props: { views: { id: string, title: string, query?: any }[] }) => {
|
|
||||||
const studioStore = useStudioStore();
|
|
||||||
const currentViewId = studioStore.currentView?.viewId;
|
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
||||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
|
||||||
const isActiveView = (viewId: string) => {
|
|
||||||
return viewId === currentViewId;
|
|
||||||
}
|
|
||||||
return <div className="mt-2 ml-4 w-full border-l-2 border-l-gray-300 border-gray-300 pl-3 space-y-1">
|
|
||||||
{props.views.map(v => (
|
|
||||||
<div
|
|
||||||
key={v.id}
|
|
||||||
className={`text-sm px-2 py-1 rounded cursor-pointer transition-colors flex items-center justify-between group ${isActiveView(v.id) ? 'text-black bg-gray-100' : 'text-gray-600 hover:text-black hover:bg-gray-100'}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
studioStore.setCurrentView({ ...view, viewId: v.id })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span>{v.title || '未命名视图'}</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" className="max-w-xs">
|
|
||||||
<div className="text-xs">
|
|
||||||
{v.query ? v.query : '无查询字段'}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<Popover open={deleteConfirmOpen && deleteTargetId === v.id} onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setDeleteConfirmOpen(false);
|
|
||||||
setDeleteTargetId(null);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Trash2
|
|
||||||
className="h-4 w-4 text-gray-400 hover:text-gray-600 transition-colors cursor-pointer opacity-0 group-hover:opacity-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setDeleteTargetId(v.id);
|
|
||||||
setDeleteConfirmOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent side="bottom" align="end" sideOffset={8} className="w-80 border-gray-300">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-sm">删除视图</h4>
|
|
||||||
<p className="text-xs text-gray-600 mt-1">确定要删除这个视图吗?此操作无法撤销。</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button className="border-gray-300" variant="outline" size="sm" onClick={() => setDeleteConfirmOpen(false)}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (deleteTargetId) {
|
|
||||||
onDeleteViewItem(view.id, deleteTargetId);
|
|
||||||
setDeleteConfirmOpen(false);
|
|
||||||
setDeleteTargetId(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
return <div
|
|
||||||
key={view.id}
|
|
||||||
className="flex flex-col items-center py-3 px-4 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="w-full flex justify-between" onClick={() => setExpanded(!expanded)}>
|
|
||||||
<div className="flex items-center cursor-pointer" >
|
|
||||||
<Layout className="h-4 w-4 mr-2 text-gray-500" />
|
|
||||||
{view.title || '未命名视图'}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="border-gray-300">
|
|
||||||
<DropdownMenuItem className="cursor-pointer" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onEdit(view);
|
|
||||||
}}>
|
|
||||||
<Edit2 className="h-4 w-4 mr-2" />
|
|
||||||
编辑
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onDelete(view.id);
|
|
||||||
}}
|
|
||||||
className="cursor-pointer text-red-600 focus:text-red-600"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
删除
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{expanded &&
|
|
||||||
<ShowViews views={view.views} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
export const ViewList = () => {
|
|
||||||
const { routeViewList, updateRouteView, deleteRouteView, deleteRouteViewItem, getViewList } = useStudioStore();
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [editorOpen, setEditorOpen] = useState(false);
|
|
||||||
const [editingView, setEditingView] = useState<any>(null);
|
|
||||||
|
|
||||||
const filteredViews = routeViewList.filter(view =>
|
|
||||||
(view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
(view.description || '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
getViewList();
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
const toastId = toast.loading('正在刷新视图列表...');
|
|
||||||
await getViewList();
|
|
||||||
toast.update(toastId, { render: '视图列表已刷新', type: 'success', isLoading: false, autoClose: 1000 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
|
||||||
handleEdit({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (view: any) => {
|
|
||||||
setEditingView(view);
|
|
||||||
setEditorOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
if (confirm('确定要删除这个视图吗?')) {
|
|
||||||
deleteRouteView(id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveView = (viewData: any) => {
|
|
||||||
updateRouteView(viewData);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full max-w-4xl p-4 border border-gray-200 rounded-md shadow-sm">
|
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Input
|
|
||||||
placeholder="搜索视图..."
|
|
||||||
className="pl-3 pr-8"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" size="icon" className="h-10 w-10 cursor-pointer border-gray-300" onClick={handleRefresh}>
|
|
||||||
<RotateCw className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="icon" className="h-10 w-10 cursor-pointer border-gray-300" onClick={handleAdd}>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{filteredViews.length === 0 ? (
|
|
||||||
<div className="text-center py-4 text-gray-500">
|
|
||||||
{searchTerm ? '未找到匹配的视图' : '暂无视图'}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredViews.map((view) => (
|
|
||||||
<ViewItem
|
|
||||||
key={view.id}
|
|
||||||
view={view}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onDeleteViewItem={deleteRouteViewItem}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ViewEditor
|
|
||||||
open={editorOpen}
|
|
||||||
onOpenChange={setEditorOpen}
|
|
||||||
data={editingView}
|
|
||||||
onSave={handleSaveView}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Marked } from 'marked';
|
|
||||||
import hljs from 'highlight.js';
|
|
||||||
import { markedHighlight } from 'marked-highlight';
|
|
||||||
|
|
||||||
const markedAndHighlight = new Marked(
|
|
||||||
markedHighlight({
|
|
||||||
emptyLangClass: 'hljs',
|
|
||||||
langPrefix: 'hljs language-',
|
|
||||||
highlight(code, lang, info) {
|
|
||||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
||||||
return hljs.highlight(code, { language }).value;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const md2html = async (md: string) => {
|
|
||||||
const html = markedAndHighlight.parse(md);
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearMeta = (markdown?: string) => {
|
|
||||||
if (!markdown) return '';
|
|
||||||
// Remove YAML front matter if present
|
|
||||||
const yamlRegex = /^---\n[\s\S]*?\n---\n/;
|
|
||||||
return markdown.replace(yamlRegex, '');
|
|
||||||
};
|
|
||||||
type Props = {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
content?: string; // Optional content prop for markdown text
|
|
||||||
[key: string]: any; // Allow any additional props
|
|
||||||
};
|
|
||||||
export const MarkdownPreview = (props: Props) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'markdown-body scrollbar h-full overflow-auto w-full px-6 py-2 max-w-[800px] border my-4 flex flex-col justify-self-center rounded-md shadow-md',
|
|
||||||
props.className,
|
|
||||||
)}
|
|
||||||
style={props.style}>
|
|
||||||
{props.children ? <WrapperText>{props.children}</WrapperText> : <MarkdownPreviewWrapper content={clearMeta(props.content)} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WrapperText = (props: { children?: React.ReactNode; html?: string }) => {
|
|
||||||
if (props.html) {
|
|
||||||
return <div className='w-full' dangerouslySetInnerHTML={{ __html: props.html }} />;
|
|
||||||
}
|
|
||||||
return <div className='w-full h-full'>{props.children}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MarkdownPreviewWrapper = (props: Props) => {
|
|
||||||
const [html, setHtml] = useState<string>('');
|
|
||||||
useEffect(() => {
|
|
||||||
init();
|
|
||||||
}, [props.content]);
|
|
||||||
const init = async () => {
|
|
||||||
if (props.content) {
|
|
||||||
const htmlContent = await md2html(props.content);
|
|
||||||
setHtml(htmlContent);
|
|
||||||
} else {
|
|
||||||
setHtml('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return <WrapperText html={html} />;
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
import '../styles/global.css';
|
|
||||||
import '../styles/theme.css';
|
|
||||||
export interface Props {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
lang?: string;
|
|
||||||
charset?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title = 'Light Code', description = 'A lightweight code editor', lang = 'zh-CN', charset = 'UTF-8' } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang={lang}>
|
|
||||||
<head>
|
|
||||||
<meta charset={charset} />
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='description' content={description} />
|
|
||||||
<title>{title}</title>
|
|
||||||
<!-- 样式 -->
|
|
||||||
<slot name='head' />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
<!-- 脚本 -->
|
|
||||||
<slot name='scripts' />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
data-variant={variant}
|
|
||||||
data-size={size}
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
||||||
import { CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Checkbox({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
data-slot="checkbox"
|
|
||||||
className={cn(
|
|
||||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
data-slot="checkbox-indicator"
|
|
||||||
className="grid place-content-center text-current transition-none"
|
|
||||||
>
|
|
||||||
<CheckIcon className="size-3.5" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox }
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
|
|
||||||
const Dialog = ({ open, onOpenChange, children }: { open: boolean; onOpenChange: (open: boolean) => void; children: React.ReactNode }) => {
|
|
||||||
if (!open) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
||||||
<div className="fixed inset-0 bg-black/50" onClick={() => onOpenChange(false)} />
|
|
||||||
<div className="relative z-50 w-full max-w-2xl max-h-[90vh] overflow-auto bg-white rounded-lg shadow-lg p-6">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DialogHeader = ({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left mb-4", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const DialogTitle = ({ className, children, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
|
||||||
<h2 className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</h2>
|
|
||||||
)
|
|
||||||
|
|
||||||
const DialogContent = ({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const DialogFooter = ({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-4", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export { Dialog, DialogHeader, DialogTitle, DialogContent, DialogFooter }
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function DropdownMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Trigger
|
|
||||||
data-slot="dropdown-menu-trigger"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
data-slot="dropdown-menu-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
data-slot="dropdown-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
|
||||||
data-slot="dropdown-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
data-slot="dropdown-menu-radio-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className="size-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
data-slot="dropdown-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
data-slot="dropdown-menu-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="dropdown-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
data-slot="dropdown-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
"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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label }
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Popover({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverContent({
|
|
||||||
className,
|
|
||||||
align = "center",
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
data-slot="popover-content"
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverAnchor({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
|
||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="table-container"
|
|
||||||
className="relative w-full overflow-x-auto"
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
data-slot="table"
|
|
||||||
className={cn("w-full caption-bottom text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
||||||
return (
|
|
||||||
<thead
|
|
||||||
data-slot="table-header"
|
|
||||||
className={cn("[&_tr]:border-b", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
||||||
return (
|
|
||||||
<tbody
|
|
||||||
data-slot="table-body"
|
|
||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
||||||
return (
|
|
||||||
<tfoot
|
|
||||||
data-slot="table-footer"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
data-slot="table-row"
|
|
||||||
className={cn(
|
|
||||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
||||||
return (
|
|
||||||
<th
|
|
||||||
data-slot="table-head"
|
|
||||||
className={cn(
|
|
||||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
data-slot="table-cell"
|
|
||||||
className={cn(
|
|
||||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableCaption({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"caption">) {
|
|
||||||
return (
|
|
||||||
<caption
|
|
||||||
data-slot="table-caption"
|
|
||||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Table,
|
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableCaption,
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Tabs({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot="tabs"
|
|
||||||
className={cn("flex flex-col gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
data-slot="tabs-list"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
data-slot="tabs-trigger"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
data-slot="tabs-content"
|
|
||||||
className={cn("flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function TooltipProvider({
|
|
||||||
delayDuration = 0,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Provider
|
|
||||||
data-slot="tooltip-provider"
|
|
||||||
delayDuration={delayDuration}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tooltip({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
||||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 0,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Content
|
|
||||||
data-slot="tooltip-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
||||||
</TooltipPrimitive.Content>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
count: {
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const counter = ref(props.count)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex w-min border border-main rounded-md">
|
|
||||||
<button
|
|
||||||
class="border-r border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
|
||||||
@click="counter -= 1"
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<span class="m-auto p-2">{{ counter }}</span>
|
|
||||||
<button
|
|
||||||
class="border-l border-main p-2 font-mono outline-none hover:bg-gray-400 hover:bg-opacity-20"
|
|
||||||
@click="counter += 1"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
import { defineCollection, z } from 'astro:content';
|
|
||||||
import { glob, file } from 'astro/loaders'; // 不适用于旧版 API
|
|
||||||
|
|
||||||
const docs = defineCollection({
|
|
||||||
loader: glob({ pattern: '**/[^_]*.md', base: './src/data/docs' }),
|
|
||||||
schema: z.object({
|
|
||||||
title: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
tags: z.array(z.string()).optional(),
|
|
||||||
// pubDate: z.coerce.date(),
|
|
||||||
createdAt: z.coerce.date().optional(),
|
|
||||||
updatedAt: z.coerce.date().optional(),
|
|
||||||
showMenu: z.boolean().optional().default(true),
|
|
||||||
/**
|
|
||||||
* 在侧边栏隐藏该文档
|
|
||||||
*/
|
|
||||||
hideInMenu: z.boolean().optional().default(false),
|
|
||||||
order: z.number().optional().default(0),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const collections = { docs };
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'astro 概览'
|
|
||||||
tags: ['astro', 'simple', 'template']
|
|
||||||
createdAt: '2025-11-25 20:00:00'
|
|
||||||
hideInMenu: true
|
|
||||||
---
|
|
||||||
|
|
||||||
## 概览
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: '想要模板做什么'
|
|
||||||
tags: ['astro', 'simple', 'template']
|
|
||||||
createdAt: '2025-11-25 20:00:00'
|
|
||||||
hideInMenu: true
|
|
||||||
---
|
|
||||||
|
|
||||||
## 模板介绍
|
|
||||||
|
|
||||||
合并文档内容和开发项目的模块
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { app } from '@/app.ts';
|
|
||||||
|
|
||||||
import './routes/left-panel.js';
|
|
||||||
|
|
||||||
export { app };
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
import '../styles/global.css';
|
|
||||||
export interface Props {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
import 'github-markdown-css/github-markdown-light.css';
|
|
||||||
import 'highlight.js/styles/github-dark.css';
|
|
||||||
const { title, description } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>{title || '文档'}</title>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50 min-h-screen">
|
|
||||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
|
||||||
<article class="markdown-body bg-white rounded-lg shadow-lg p-8">
|
|
||||||
<slot />
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
import '../styles/global.css';
|
|
||||||
import '../styles/theme.css';
|
|
||||||
export interface Props {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
lang?: string;
|
|
||||||
charset?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title = 'Light Code', description = 'A lightweight code editor', lang = 'zh-CN', charset = 'UTF-8' } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang={lang}>
|
|
||||||
<head>
|
|
||||||
<meta charset={charset} />
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<meta name='description' content={description} />
|
|
||||||
<title>{title}</title>
|
|
||||||
<!-- 样式 -->
|
|
||||||
<slot name='head' />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
<!-- 脚本 -->
|
|
||||||
<slot name='scripts' />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
分页组件
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
export interface Props {
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
import '../styles/global.css';
|
|
||||||
import '../styles/theme.css';
|
|
||||||
import 'github-markdown-css/github-markdown-light.css';
|
|
||||||
import { Menu, MenuItem } from '../apps/menu';
|
|
||||||
export interface Props {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
lang?: string;
|
|
||||||
charset?: string;
|
|
||||||
showMenu?: boolean;
|
|
||||||
menu?: MenuItem[];
|
|
||||||
basename?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
title = 'Light Code',
|
|
||||||
description = 'A lightweight code editor',
|
|
||||||
lang = 'zh-CN',
|
|
||||||
charset = 'UTF-8',
|
|
||||||
showMenu = true,
|
|
||||||
menu,
|
|
||||||
basename = '',
|
|
||||||
} = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<html lang={lang}>
|
|
||||||
<head>
|
|
||||||
<meta charset={charset} />
|
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
|
||||||
<title>{title}</title>
|
|
||||||
<meta name='description' content={description} />
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class='flex flex-col items-center bg-background'>
|
|
||||||
<div class='w-full'>
|
|
||||||
<slot name='header' />
|
|
||||||
</div>
|
|
||||||
<main class='flex-1 flex overflow-hidden w-full max-w-7xl px-4 py-4'>
|
|
||||||
{
|
|
||||||
showMenu && (
|
|
||||||
<aside class='w-64 min-w-64 h-full flex flex-col'>
|
|
||||||
<slot name='menu'>
|
|
||||||
<Menu items={menu!} client:only basename={basename} />
|
|
||||||
</slot>
|
|
||||||
</aside>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div class='flex-1 h-full flex items-start justify-center overflow-hidden'>
|
|
||||||
<article class='markdown-body h-full scrollbar overflow-auto px-8 py-6 w-full max-w-4xl border border-border rounded-lg shadow-sm bg-card'>
|
|
||||||
<slot />
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer class='w-full border-t border-border bg-card/50 backdrop-blur-sm'>
|
|
||||||
<slot name='footer'>
|
|
||||||
<div class='text-center text-sm text-muted-foreground py-4'>Copyright © 2025</div>
|
|
||||||
</slot>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
export const basename = BASE_NAME;
|
|
||||||
|
|
||||||
console.log(basename);
|
|
||||||
|
|
||||||
export const wrapBasename = (path: string) => {
|
|
||||||
const hasEnd = path.endsWith('/')
|
|
||||||
if (basename) {
|
|
||||||
return `${basename}${path}` + (hasEnd ? '' : '/');
|
|
||||||
} else {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { QueryClient } from '@kevisual/query'
|
|
||||||
|
|
||||||
const getUrl = () => {
|
|
||||||
const host = window.location.host
|
|
||||||
const isKevisual = host.includes('kevisual');
|
|
||||||
if (isKevisual) {
|
|
||||||
return '/api/router'
|
|
||||||
}
|
|
||||||
|
|
||||||
return '/client/router'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const query = new QueryClient({
|
|
||||||
url: '/api/router',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
url: getUrl(),
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
import Html from '@/components/html.astro';
|
|
||||||
import { AppProvider } from '@/apps/cv/index.tsx';
|
|
||||||
---
|
|
||||||
|
|
||||||
<Html title='简历'>
|
|
||||||
<AppProvider client:only></AppProvider>
|
|
||||||
</Html>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
import Html from '@/components/html.astro';
|
|
||||||
---
|
|
||||||
|
|
||||||
<Html>
|
|
||||||
<main>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</Html>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
import Html from '@/components/html.astro';
|
|
||||||
// import Counter from '@/components/vue/Counter.vue';
|
|
||||||
---
|
|
||||||
|
|
||||||
<Html>
|
|
||||||
<main>
|
|
||||||
<!-- <Counter count={10} client:only/> -->
|
|
||||||
</main>
|
|
||||||
</Html>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
import { getCollection, render } from 'astro:content';
|
|
||||||
import Main from '@/layouts/mdx.astro';
|
|
||||||
import { basename } from '@/modules/basename';
|
|
||||||
// 1. 为每个集合条目生成一个新路径
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const posts = await getCollection('docs');
|
|
||||||
return posts.map((post) => ({
|
|
||||||
params: { id: post.id },
|
|
||||||
props: { post },
|
|
||||||
data: post,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
type Post = {
|
|
||||||
data: { title: string; tags: string[]; showMenu?: boolean };
|
|
||||||
};
|
|
||||||
// 2. 对于你的模板,你可以直接从 prop 获取条目
|
|
||||||
const { post } = Astro.props as { post: Post };
|
|
||||||
const { Content } = await render(post);
|
|
||||||
const showMenu = post.data?.showMenu;
|
|
||||||
const staticPaths = await getStaticPaths();
|
|
||||||
const menu = staticPaths.map((item) => item.data);
|
|
||||||
---
|
|
||||||
|
|
||||||
<Main showMenu={showMenu} menu={menu} basename={basename} title={post.data.title}>
|
|
||||||
<Content />
|
|
||||||
</Main>
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
---
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
const posts = await getCollection('docs');
|
|
||||||
import { basename, wrapBasename } from '@/modules/basename';
|
|
||||||
import Blank from '@/layouts/blank.astro';
|
|
||||||
---
|
|
||||||
|
|
||||||
<Blank>
|
|
||||||
<main class='min-h-screen bg-linear-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800'>
|
|
||||||
<div class='max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12'>
|
|
||||||
{/* 页面标题区域 */}
|
|
||||||
<div class='mb-12'>
|
|
||||||
<h1 class='text-4xl sm:text-5xl font-bold text-slate-900 dark:text-white mb-4 bg-clip-text bg-linear-to-r from-blue-600 to-purple-600'>📚 文档列表</h1>
|
|
||||||
<p class='text-slate-600 dark:text-slate-400 text-lg'>浏览所有可用的文档资源</p>
|
|
||||||
<div class='mt-4 h-1 w-20 bg-linear-to-r from-blue-600 to-purple-600 rounded-full'></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 文档列表 */}
|
|
||||||
<div class='space-y-4'>
|
|
||||||
{
|
|
||||||
posts.map((post) => {
|
|
||||||
const tags = post.data.tags || [];
|
|
||||||
const postUrl = wrapBasename(`/docs/${post.id}`);
|
|
||||||
return (
|
|
||||||
<article class='group bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden border border-slate-200 dark:border-slate-700 hover:border-blue-500 dark:hover:border-blue-400'>
|
|
||||||
<div class='p-6'>
|
|
||||||
{/* 文档标题 */}
|
|
||||||
<a href={postUrl} class='block'>
|
|
||||||
<h2 class='text-xl sm:text-2xl font-semibold text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors duration-200 mb-3'>
|
|
||||||
{post.data.title}
|
|
||||||
</h2>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* 文档描述(如果有) */}
|
|
||||||
{post.data.description && <p class='text-slate-600 dark:text-slate-400 mb-4 line-clamp-2'>{post.data.description}</p>}
|
|
||||||
|
|
||||||
{/* 标签列表 */}
|
|
||||||
{tags.length > 0 && (
|
|
||||||
<div class='flex flex-wrap gap-2 mt-4'>
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<div class='inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors duration-200 border border-blue-200 dark:border-blue-800'>
|
|
||||||
<span class='mr-1'>#</span>
|
|
||||||
{tag}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 阅读更多指示器 */}
|
|
||||||
<a
|
|
||||||
href={postUrl}
|
|
||||||
class='mt-4 flex items-center text-blue-600 dark:text-blue-400 text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200'>
|
|
||||||
<span>阅读更多</span>
|
|
||||||
<svg
|
|
||||||
class='w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform duration-200'
|
|
||||||
fill='none'
|
|
||||||
viewBox='0 0 24 24'
|
|
||||||
stroke='currentColor'>
|
|
||||||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 5l7 7-7 7' />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 空状态 */}
|
|
||||||
{
|
|
||||||
posts.length === 0 && (
|
|
||||||
<div class='text-center py-16'>
|
|
||||||
<div class='text-6xl mb-4'>📭</div>
|
|
||||||
<p class='text-xl text-slate-600 dark:text-slate-400'>暂无文档</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</Blank>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
import Html from '@/components/html.astro';
|
|
||||||
import { AppProvider } from '@/apps/studio/index.tsx';
|
|
||||||
---
|
|
||||||
|
|
||||||
<Html title='Router Studio'>
|
|
||||||
<AppProvider client:only></AppProvider>
|
|
||||||
</Html>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
import Html from '@/components/html.astro';
|
|
||||||
import { AppProvider } from '@/apps/query-view/index.tsx';
|
|
||||||
---
|
|
||||||
|
|
||||||
<Html title='Router Studio'>
|
|
||||||
<AppProvider client:only />
|
|
||||||
</Html>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { app } from '@/app.ts';
|
|
||||||
import { use } from '@kevisual/context'
|
|
||||||
|
|
||||||
app.route({
|
|
||||||
path: 'web',
|
|
||||||
key: 'togglePanel',
|
|
||||||
description: '当前的网页页面功能,切换左侧面板显示与隐藏',
|
|
||||||
metadata: {
|
|
||||||
tags: ['web', 'studio', 'page'],
|
|
||||||
}
|
|
||||||
}).define(async (ctx) => {
|
|
||||||
const store = use('studioStore');
|
|
||||||
try {
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
state.setShowLeftPanel(!state.showLeftPanel);
|
|
||||||
ctx.body = { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
ctx.body = { success: false, message: (error as Error).message };
|
|
||||||
}
|
|
||||||
|
|
||||||
}).addTo(app)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
@import "tw-animate-css";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--background: oklch(1 0 0);
|
|
||||||
--foreground: oklch(0.145 0 0);
|
|
||||||
--card: oklch(1 0 0);
|
|
||||||
--card-foreground: oklch(0.145 0 0);
|
|
||||||
--popover: oklch(1 0 0);
|
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
|
||||||
--primary: oklch(0.205 0 0);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
|
||||||
--secondary: oklch(0.97 0 0);
|
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
|
||||||
--muted: oklch(0.97 0 0);
|
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
|
||||||
--accent: oklch(0.97 0 0);
|
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.922 0 0);
|
|
||||||
--input: oklch(0.922 0 0);
|
|
||||||
--ring: oklch(0.708 0 0);
|
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0.145 0 0);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.205 0 0);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.205 0 0);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.922 0 0);
|
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.269 0 0);
|
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
|
||||||
--accent: oklch(0.269 0 0);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
|
||||||
--input: oklch(1 0 0 / 15%);
|
|
||||||
--ring: oklch(0.556 0 0);
|
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--sidebar: oklch(0.205 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@kevisual/types/json/frontend.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"./src/*"
|
|
||||||
],
|
|
||||||
"@/agent": [
|
|
||||||
"./src/agent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*",
|
|
||||||
"agent/**/*"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
65
package.json
65
package.json
@@ -1,7 +1,62 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"name": "@kevisual/router-studio",
|
||||||
"nanoid": "^5.1.6",
|
"version": "0.1.8",
|
||||||
"zod": "^4.3.6",
|
"basename": "/root/router-studio",
|
||||||
"zod-to-json-schema": "^3.25.1"
|
"scripts": {
|
||||||
}
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"ui": "bunx shadcn@latest add ",
|
||||||
|
"pub": "envision deploy ./dist -k router-studio -v 0.1.8 -y y -u"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.2.0",
|
||||||
|
"@kevisual/router": "0.0.83",
|
||||||
|
"@tanstack/react-router": "^1.161.4",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@uiw/react-md-editor": "^4.0.11",
|
||||||
|
"antd": "^6.3.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"eruda": "^3.4.3",
|
||||||
|
"es-toolkit": "^1.44.0",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
|
"idb-keyval": "^6.2.2",
|
||||||
|
"lucide-react": "^0.575.0",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"react-hook-form": "^7.71.2",
|
||||||
|
"react-resizable-panels": "^4.6.5",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"valtio": "^2.3.0",
|
||||||
|
"zod": "^4.3.6",
|
||||||
|
"zustand": "^5.0.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@kevisual/api": "^0.0.59",
|
||||||
|
"@kevisual/context": "^0.0.8",
|
||||||
|
"@kevisual/js-filter": "^0.0.5",
|
||||||
|
"@kevisual/query": "^0.0.49",
|
||||||
|
"@kevisual/types": "^0.0.12",
|
||||||
|
"@tailwindcss/vite": "^4.2.0",
|
||||||
|
"@tanstack/react-router-devtools": "^1.161.4",
|
||||||
|
"@tanstack/router-plugin": "^1.161.4",
|
||||||
|
"@types/node": "^25.3.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwindcss": "^4.2.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "v8.0.0-beta.15"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
888
pnpm-lock.yaml
generated
888
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user