update
This commit is contained in:
@@ -10,7 +10,7 @@ import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
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' };
|
||||
let proxy = {
|
||||
'/root/': apiProxy,
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@kevisual/query": "^0.0.33",
|
||||
"@kevisual/query-login": "^0.0.7",
|
||||
"@kevisual/registry": "^0.0.1",
|
||||
"@kevisual/router": "^0.0.49",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@uiw/react-md-editor": "^4.0.11",
|
||||
@@ -34,6 +35,7 @@
|
||||
"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",
|
||||
@@ -53,6 +55,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/api": "^0.0.5",
|
||||
"@kevisual/types": "^0.0.10",
|
||||
"@types/react": "^19.2.7",
|
||||
"@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';
|
||||
console.log('Hello from index.astro');
|
||||
import '../styles/global.css';
|
||||
import Html from '@/components/html.astro';
|
||||
import { AppProvider } from '@/apps/studio/index.tsx';
|
||||
---
|
||||
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>My Homepage</title>
|
||||
</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>
|
||||
<Html title='Router Studio'>
|
||||
<AppProvider client:only></AppProvider>
|
||||
</Html>
|
||||
|
||||
Reference in New Issue
Block a user