update
This commit is contained in:
@@ -10,7 +10,7 @@ import dotenv from 'dotenv';
|
|||||||
dotenv.config();
|
dotenv.config();
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
let target = process.env.VITE_API_URL || 'http://localhost:51015';
|
let target = process.env.VITE_API_URL || 'http://localhost:51515';
|
||||||
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||||
let proxy = {
|
let proxy = {
|
||||||
'/root/': apiProxy,
|
'/root/': apiProxy,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@kevisual/query": "^0.0.33",
|
"@kevisual/query": "^0.0.33",
|
||||||
"@kevisual/query-login": "^0.0.7",
|
"@kevisual/query-login": "^0.0.7",
|
||||||
"@kevisual/registry": "^0.0.1",
|
"@kevisual/registry": "^0.0.1",
|
||||||
|
"@kevisual/router": "^0.0.49",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@uiw/react-md-editor": "^4.0.11",
|
"@uiw/react-md-editor": "^4.0.11",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"astro": "^5.16.6",
|
"astro": "^5.16.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"es-toolkit": "^1.43.0",
|
"es-toolkit": "^1.43.0",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@kevisual/api": "^0.0.5",
|
||||||
"@kevisual/types": "^0.0.10",
|
"@kevisual/types": "^0.0.10",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
798
web/pnpm-lock.yaml
generated
798
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
web/src/app.ts
Normal file
3
web/src/app.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { QueryRouterServer } from '@kevisual/router/browser'
|
||||||
|
import { use } from '@kevisual/context'
|
||||||
|
export const app = use('app', new QueryRouterServer())
|
||||||
257
web/src/apps/cv/index.css
Normal file
257
web/src/apps/cv/index.css
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
.cv-app {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-header {
|
||||||
|
height: 68px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 0 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #0a0a0a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn-primary {
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: white;
|
||||||
|
border-color: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn-primary:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn-secondary {
|
||||||
|
background: white;
|
||||||
|
color: #525252;
|
||||||
|
border-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn-secondary:hover {
|
||||||
|
background: #fafafa;
|
||||||
|
border-color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn-danger {
|
||||||
|
background: white;
|
||||||
|
color: #dc2626;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn-danger:hover {
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-editor-container {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-preview-container {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-preview {
|
||||||
|
background: white;
|
||||||
|
padding: 3rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
min-height: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown 样式优化 */
|
||||||
|
.markdown-body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body ul,
|
||||||
|
.markdown-body ol {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 85%;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 85%;
|
||||||
|
line-height: 1.45;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
color: #525252;
|
||||||
|
border-left: 0.25em solid #e5e5e5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
height: 0.25em;
|
||||||
|
padding: 0;
|
||||||
|
margin: 24px 0;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 打印样式 */
|
||||||
|
@media print {
|
||||||
|
.cv-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-app {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-preview {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-preview-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cv-header {
|
||||||
|
height: auto;
|
||||||
|
min-height: 68px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-btn span:not([class]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-preview {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cv-editor-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
web/src/apps/cv/index.tsx
Normal file
174
web/src/apps/cv/index.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import MDEditor from '@uiw/react-md-editor'
|
||||||
|
import { ToastContainer } 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 />
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
152
web/src/apps/studio/index.tsx
Normal file
152
web/src/apps/studio/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { toast, ToastContainer } from 'react-toastify';
|
||||||
|
import { useStudioStore } from './store.ts';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Play } from 'lucide-react';
|
||||||
|
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" />
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouteItem {
|
||||||
|
id: string;
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
description?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
const { routes, getRoutes, run } = useStudioStore();
|
||||||
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||||
|
const [visibleIds, setVisibleIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getRoutes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center p-1.5 rounded-md text-gray-500 hover:text-green-600 hover:bg-green-50 transition-all duration-200 cursor-pointer"
|
||||||
|
title="运行"
|
||||||
|
onClick={() => run(route)}
|
||||||
|
>
|
||||||
|
<Play size={14} strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description with expand/collapse */}
|
||||||
|
{route.description && (
|
||||||
|
<div
|
||||||
|
className="cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`text-gray-700 transition-colors duration-200 cursor-pointer ${isExpanded ? 'text-gray-900' : 'group-hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
web/src/apps/studio/store.ts
Normal file
47
web/src/apps/studio/store.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { QueryProxy } from '@kevisual/api'
|
||||||
|
// import { query } from '@/modules/query.ts'
|
||||||
|
import { Query } from '@kevisual/query';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
const url = localStorage.getItem('BROWSER_HELPER_URL') || 'http://localhost:52000/api/router';
|
||||||
|
const query = new Query({
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
import { QueryRouterServer } from '@kevisual/router/src/route.ts'
|
||||||
|
|
||||||
|
const router = new QueryRouterServer();
|
||||||
|
const qp = new QueryProxy({ query, router });
|
||||||
|
|
||||||
|
type RouteItem = {
|
||||||
|
id: string;
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
description?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
interface StudioState {
|
||||||
|
routes: Array<RouteItem>;
|
||||||
|
getRoutes: () => Promise<void>;
|
||||||
|
run: (route: RouteItem) => Promise<void>;
|
||||||
|
}
|
||||||
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
export const useStudioStore = create<StudioState>((set) => ({
|
||||||
|
routes: [],
|
||||||
|
getRoutes: async () => {
|
||||||
|
await qp.init();
|
||||||
|
await sleep(200); // wait for routes to be registered
|
||||||
|
console.log('query proxy', qp.router);
|
||||||
|
const routes: any[] = await qp.listRoutes()
|
||||||
|
console.log('fetched routes', routes);
|
||||||
|
set({ routes });
|
||||||
|
},
|
||||||
|
run: async (route: RouteItem) => {
|
||||||
|
console.log('running route', route);
|
||||||
|
const res = await qp.run({ path: route.path, key: route.key });
|
||||||
|
console.log('route run result', res);
|
||||||
|
if (res.code !== 200) {
|
||||||
|
toast.error(`运行失败:${res.message || '未知错误'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
8
web/src/pages/cv.astro
Normal file
8
web/src/pages/cv.astro
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import Html from '@/components/html.astro';
|
||||||
|
import { AppProvider } from '@/apps/cv/index.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Html title='简历'>
|
||||||
|
<AppProvider client:only></AppProvider>
|
||||||
|
</Html>
|
||||||
@@ -1,47 +1,8 @@
|
|||||||
---
|
---
|
||||||
// import { query } from '@/modules/query.ts';
|
import Html from '@/components/html.astro';
|
||||||
console.log('Hello from index.astro');
|
import { AppProvider } from '@/apps/studio/index.tsx';
|
||||||
import '../styles/global.css';
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang='en'>
|
<Html title='Router Studio'>
|
||||||
<head>
|
<AppProvider client:only></AppProvider>
|
||||||
<title>My Homepage</title>
|
</Html>
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1 onclick="{onClick}">Welcome to my website!</h1>
|
|
||||||
<div class='bg-amber-50 w-20 h-20 rounded-full'></div>
|
|
||||||
<div id='root'></div>
|
|
||||||
<script type='importmap' data-vite-ignore is:inline>
|
|
||||||
{
|
|
||||||
"imports": {
|
|
||||||
"react": "https://esm.sh/react@19.1.0",
|
|
||||||
"react-dom": "https://esm.sh/react-dom@19.1.0/client.js",
|
|
||||||
"react-toastify": "https://esm.sh/react-toastify@11.0.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type='module' data-vite-ignore is:inline>
|
|
||||||
import { Button, message } from 'https://esm.sh/antd?standalone';
|
|
||||||
import React from 'react';
|
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
|
||||||
import { createRoot } from 'react-dom';
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.loading('Hello from index.astro');
|
|
||||||
window.toast = toast;
|
|
||||||
console.log('message', toast);
|
|
||||||
}, 1000);
|
|
||||||
console.log('Hello from index.astro', Button);
|
|
||||||
const root = document.getElementById('root');
|
|
||||||
const render = createRoot(root);
|
|
||||||
const App = () => {
|
|
||||||
const button = React.createElement(Button, null, 'Hello');
|
|
||||||
const messageEl = React.createElement(ToastContainer, null, 'Hello');
|
|
||||||
const wrapperMessage = React.createElement('div', null, [button, messageEl]);
|
|
||||||
return wrapperMessage;
|
|
||||||
};
|
|
||||||
// render.render(React.createElement(Button, null, 'Hello'), root);
|
|
||||||
render.render(App(), root);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user