Initial commit: Copilot Toolbox template project

This commit is contained in:
Evan
2026-01-09 10:49:51 +08:00
commit 0b4b053566
31 changed files with 12085 additions and 0 deletions

18
.env.development Normal file
View File

@@ -0,0 +1,18 @@
# 网站标题
VITE_DOCUMENT_TITLE='AI办公助手'
# 服务前缀
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
VITE_API_BASE_NOTE=/langwell-api/langwell-notes-server
VITE_API_BASE_PUB=/langwell-api/langwell-pub-server
VITE_API_BASE_INS=/langwell-api/langwell-ins-server
VITE_API_BASE_TOOL=/copilot-tool
VITE_API_BASE_LAMP=/lamp-api
VITE_DIRECTION_API = /direction-api
# 语音识别
VITE_API_BASE_VOICE=/voice-api
# AI后端请求地址
VITE_AI_API_BASE=/v1
# Xiren-Lite SDK 配置 - 开发环境使用代理
VITE_XIREN_LITE_BASE_URL=/xiren-api

18
.env.production Normal file
View File

@@ -0,0 +1,18 @@
# 网站标题
VITE_DOCUMENT_TITLE='AI办公助手'
# 服务前缀
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
VITE_API_BASE_NOTE=/langwell-api/langwell-notes-server
VITE_API_BASE_PUB=/langwell-api/langwell-pub-server
VITE_API_BASE_INS=/langwell-api/langwell-ins-server
VITE_API_BASE_TOOL=/copilot-tool
VITE_API_BASE_LAMP=/lamp-api
VITE_DIRECTION_API = /direction-api
# 语音识别
VITE_API_BASE_VOICE=/voice-api
# AI后端请求地址
VITE_AI_API_BASE=/v1
# Xiren-Lite SDK 配置 - 开发环境使用代理
VITE_XIREN_LITE_BASE_URL=/xiren-api

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.history
# Editor directories and files
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
dist.zip
copilot-toolbox.zip
copilot-toolbox/
.trae

87
CLAUDE.md Normal file
View File

@@ -0,0 +1,87 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Copilot Toolbox is a React + TypeScript + Vite frontend template for building modern AI-powered applications. Integrates Dify AI, CopilotKit with Ant Design and Tailwind CSS.
**This is a template project - all development must extend this project, not create new repositories.**
## Tech Stack
- React 18 + TypeScript + Vite 6
- Ant Design 5.x (UI components)
- Tailwind CSS 4.x + Less (styling)
- React Router DOM 6.x (Hash mode routing)
- @copilotkit/react-core, @ag-ui/client, @shangzy/ag-ui-dify (AI integration)
## Directory Conventions
| Type | Path Pattern | Example |
|------|--------------|---------|
| Pages | `src/pages/[name]/index.tsx` | `src/pages/home/index.tsx` |
| API | `src/api/[name].ts` | `src/api/user.ts` |
| Components | `src/components/[name]/index.tsx` | `src/components/Header/index.tsx` |
| Utils | `src/utils/[name].ts` | `src/utils/cacheUtil.ts` |
- Use `@/` alias for imports (configured in vite.config.ts)
- TypeScript strict mode enabled
- Tailwind CSS for styling, Ant Design for components
## Common Commands
```bash
pnpm install # Install dependencies
pnpm dev # Dev server (port 5173)
pnpm build # Production build
pnpm preview # Preview production build (port 3000)
```
## Environment Variables
All API base paths use proxy prefixes. Set in `.env.development` or `.env.production`:
```
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
VITE_API_BASE_LAMP=/lamp-api
VITE_AI_API_BASE=/v1
```
## API Pattern
```typescript
// src/api/[name].ts
export function getData(params: { id: string }) {
return fetch(`${import.meta.env.VITE_API_BASE_AI}/endpoint`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json())
}
```
## Routing
Routes defined in `src/router/index.tsx` using lazy loading:
```typescript
{
path: '/page-name',
element: LazyLoad(lazy(() => import('@/pages/page-name')))
}
```
## Adding New Features
1. Create page: `src/pages/[name]/index.tsx`
2. Create API: `src/api/[name].ts`
3. Add route: `src/router/index.tsx`
4. Use `@/` alias for imports
## Restrictions
- Do NOT create new repositories - extend this project
- Do NOT remove CLAUDE.md
- Use `@/` alias, not relative paths
- TypeScript strict mode cannot be disabled

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM node:24-alpine
WORKDIR /app
# 配置阿里云 npm 镜像
RUN npm config set registry https://registry.npmmirror.com
# 复制依赖文件
COPY package*.json ./
# 安装所有依赖(包括 devDependencies
RUN npm install
# 复制源代码
COPY . .
# 暴露端口
EXPOSE 3000
# 运行开发服务器
CMD ["npm", "run", "dev", "--", "--port", "3000"]

0
README.md Normal file
View File

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
services:
app:
build: .
container_name: copilot-toolbox-dev
restart: unless-stopped
expose:
- "3000"

25
index.html Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>AI办公助手</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script>
window.iFrameResizer = {
heightCalculationMethod: 'lowestElement'
}
</script>
<script src="/iframeResizer.contentWindow.js"></script>
</body>
</html>

5696
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "copilot-toolbox",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build --mode production",
"preview": "vite preview"
},
"dependencies": {
"@ag-ui/client": "^0.0.42",
"@ant-design/icons": "^5.5.2",
"@ant-design/pro-components": "^2.8.10",
"@copilotkit/react-core": "1.10.6",
"@shangzy/ag-ui-dify": "^0.1.0",
"antd": "^5.23.0",
"dayjs": "^1.11.13",
"less": "^4.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.1"
},
"devDependencies": {
"@tailwindcss/vite": "4.0.12",
"@types/node": "^22.10.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"path": "^0.12.7",
"prettier-plugin-tailwindcss": "^0.1.7",
"tailwindcss": "4.0.12",
"typescript": "~5.6.3",
"vite": "^6.0.7"
}
}

3968
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

44
src/App.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { RouterProvider } from 'react-router-dom'
import { useEffect } from 'react'
import router from '@/router'
import { cacheClear, cacheSet } from '@/utils/cacheUtil'
import { getAgentBaseInfo, getUserInfoById } from '@/api/common'
function App() {
const queryString = window.location.search
const searchParams = new URLSearchParams(queryString)
const tenantId = searchParams.get('tenantId') || ''
const token = searchParams.get('token') || ''
cacheSet('tenantId', tenantId)
cacheSet('token', token)
useEffect(() => {
const agentId = searchParams.get('agentId') || ''
if (agentId) {
getAgentBaseInfo({ id: agentId }).then(res => {
cacheSet('appKey', res.data.appKey)
})
}
}, [])
useEffect(() => {
if (tenantId && token) {
getUserInfoById().then(response => {
if (response.code === 200 && response.data) {
cacheSet('userInfo', JSON.stringify(response.data))
}
})
}
return () => {
cacheClear('userInfo')
}
}, [])
return (
<>
<RouterProvider router={router} />
</>
)
}
export default App

28
src/api/common.ts Normal file
View File

@@ -0,0 +1,28 @@
import { cacheGet } from '@/utils/cacheUtil'
export function getAgentBaseInfo(params: { id: string }) {
const Tenantid = cacheGet('tenantId')
const Token = cacheGet('token')
return fetch(`${import.meta.env.VITE_API_BASE_AI}/agent/baseInfo/info?id=${params.id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Tenantid,
Token
}
}).then(res => res.json())
}
// 获取用户信息
export async function getUserInfoById() {
const Tenantid = cacheGet('tenantId')
const Token = cacheGet('token')
return fetch(`${import.meta.env['VITE_API_BASE_LAMP']}/oauth/anyone/getUserInfoById`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Tenantid,
Token
}
}).then(res => res.json())
}

View File

@@ -0,0 +1,34 @@
export interface TranslationRequest {
source_content: string
}
export interface DifyRequest {
inputs: {
prompt: string
}
query: string
response_mode: string
}
export function translateChineseToEnglish(source_content: string) {
const prompt = `你是一个专业的中英翻译专家。请将下面的中文文本翻译成自然、地道、专业的英文,只返回翻译后的英文内容,不要添加任何额外的解释或格式。待翻译的中文内容是:
${source_content}`
const requestBody: DifyRequest = {
inputs: {
prompt
},
query: '1',
response_mode: 'streaming'
}
return fetch('https://copilot.sino-bridge.com/v1/chat-messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer app-Y6ekYkw3aoUV3jmfZdg24Adh'
},
body: JSON.stringify(requestBody)
})
}

29
src/index.css Normal file
View File

@@ -0,0 +1,29 @@
@import 'tailwindcss';
.route-loading {
position: fixed;
top: 50%;
left: 50%;
width: 120px;
height: 22px;
color: #514b82;
border: 2px solid;
border-radius: 20px;
transform: translate(-50%, -50%);
}
@keyframes l6 {
100% {
inset: 0;
}
}
.route-loading::before {
position: absolute;
inset: 0 100% 0 0;
margin: 2px;
content: '';
background: currentcolor;
border-radius: inherit;
animation: l6 2s infinite;
}

12
src/main.tsx Normal file
View File

@@ -0,0 +1,12 @@
import './utils/polyfills'
import { createRoot } from 'react-dom/client'
import { ConfigProvider } from 'antd'
import zhCN from 'antd/es/locale/zh_CN'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
)

57
src/pages/home/index.tsx Normal file
View File

@@ -0,0 +1,57 @@
import { Card, Row, Col, Typography, Space } from 'antd'
import { Link } from 'react-router-dom'
import { RobotOutlined, TranslationOutlined, FileTextOutlined } from '@ant-design/icons'
const { Title, Paragraph } = Typography
const cards = [
{
title: '测试页面 1',
description: 'Dify AI Agent 集成示例',
icon: <RobotOutlined style={{ fontSize: 32, color: '#1890ff' }} />,
link: '/test1'
},
{
title: '测试页面 2',
description: '功能开发中...',
icon: <FileTextOutlined style={{ fontSize: 32, color: '#52c41a' }} />,
link: '/test2'
},
{
title: '中英翻译器',
description: 'AI 驱动的翻译工具',
icon: <TranslationOutlined style={{ fontSize: 32, color: '#722ed1' }} />,
link: '/zh-en-translator'
}
]
const HomePage: React.FC = () => {
return (
<div style={{ padding: 24 }}>
<Title level={2}>使 AI </Title>
<Paragraph style={{ marginBottom: 32 }}>
使访
</Paragraph>
<Row gutter={[16, 16]}>
{cards.map((card, index) => (
<Col xs={24} sm={12} md={8} key={index}>
<Link to={card.link}>
<Card hoverable style={{ height: '100%' }}>
<Space direction='vertical' align='center' style={{ width: '100%' }}>
{card.icon}
<Title level={4} style={{ marginBottom: 0 }}>{card.title}</Title>
<Paragraph type='secondary' style={{ marginBottom: 0 }}>
{card.description}
</Paragraph>
</Space>
</Card>
</Link>
</Col>
))}
</Row>
</div>
)
}
export default HomePage

81
src/pages/test1/index.tsx Normal file
View File

@@ -0,0 +1,81 @@
import { AgUiEventType, DifyAgent } from '@shangzy/ag-ui-dify'
import { RunAgentInput, EventType, TextMessageContentEvent } from '@ag-ui/client'
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
import { useState } from 'react'
const Test1: React.FC = () => {
const [message, setMessage] = useState('')
const userInfo = getUserInfo()
const runAgent = () => {
const difyJson = {
conversation_id: '',
files: [],
query: '1312313',
appKey: cacheGet('appKey'),
inputs: {
Token: cacheGet('token'),
tenantid: cacheGet('tenantId')
},
user: userInfo?.id || 'anonymous'
}
const content: string = JSON.stringify(difyJson)
// 准备输入参数
const input: RunAgentInput = {
threadId: new Date().getTime().toString(),
runId: new Date().getTime().toString(),
messages: [
{
id: new Date().getTime().toString(),
role: 'user',
content: content
}
],
context: [],
tools: []
}
// 订阅Agent事件
new DifyAgent({
baseUrl: '/dify',
showMetadata: true
})
.run(input)
.subscribe({
next: (event: AgUiEventType) => {
console.log('🚀 ~ ChatApp ~ aa ~ event:', event)
try {
switch (event.type) {
case EventType.RUN_STARTED:
// 可以在这里处理运行开始事件
break
case EventType.TEXT_MESSAGE_START:
// 处理消息开始事件
break
case EventType.TEXT_MESSAGE_CONTENT:
const textEvent = event as TextMessageContentEvent
setMessage(prev => prev + textEvent.delta)
break
case EventType.TEXT_MESSAGE_END:
break
case EventType.RUN_FINISHED:
break
}
} catch (err) {
console.error('处理事件时出错:', err)
}
},
error: () => {},
complete: () => {}
})
}
return (
<>
<div onClick={() => runAgent()}></div>
<div>{message}</div>
</>
)
}
export default Test1

158
src/pages/test2/index.tsx Normal file
View File

@@ -0,0 +1,158 @@
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
import { LoadingOutlined } from '@ant-design/icons'
import { CopilotKit, useCopilotChatInternal as useCopilotChat } from '@copilotkit/react-core'
import { Flex, Mentions, Spin } from 'antd'
import { useCallback, useState } from 'react'
const Chat: React.FC = () => {
const [currentAppKey, setCurrentAppKey] = useState<string>('')
const [newMessage, setNewMessage] = useState('')
const { messages, sendMessage, setMessages, isLoading, reloadMessages, stopGeneration } = useCopilotChat()
const callSendMessage = useCallback(
async (message: string) => {
await sendMessage({
id: new Date().getTime() + '',
role: 'user',
content: message
})
},
[sendMessage]
)
const handleSendMessage = useCallback(() => {
// 提前存好本次的提问内容,重新生成的话直接从缓存中获取之前的提问内容
let question: string = newMessage || ''
const token = cacheGet('token')
const tenantId = cacheGet('tenantId')
let conversation_id = ''
if (messages[1]?.id) {
conversation_id = messages[1]?.id.split('_')[0]
}
const userInfo = getUserInfo()
const difyJson = {
inputs: {
Token: token || '',
tenantid: tenantId || '',
query: question
},
appKey: currentAppKey,
files: [],
user: userInfo?.id || 'anonymous',
query: question,
conversation_id
}
// 设置好目前状态下的聊天列表数据包含之前已经结束的沟通内容以及本次用户的提问本次AI的回答占位
callSendMessage(JSON.stringify(difyJson))
setNewMessage('')
}, [callSendMessage, newMessage, currentAppKey])
const handleParse = (jsonStr: string) => {
let res = ''
try {
const parsed = JSON.parse(jsonStr)
res = parsed?.query
} catch (e) {}
return res
}
// 换行
const handleNewline = () => {
setNewMessage(prevValue => `${prevValue}\n`)
}
const handleKeyDown = async (e: any) => {
e.stopPropagation()
const res = await cacheGet('sendMessage')
if (e.key === 'Enter') {
if (e.ctrlKey) {
if (newMessage) {
if (res === 'ctrlEnter') {
handleSendMessage()
} else {
handleNewline()
}
}
} else {
if (res === 'Enter' || !res) {
if (newMessage) {
handleSendMessage()
} else {
setNewMessage('')
}
}
}
}
}
return (
<>
{messages.map((message, index) => {
const userMessageId = message.id
// 实际的会话消息
return (
<Flex key={index} vertical data-message-id={userMessageId}>
{/* 用户提问 */}
{message.role === 'user' && <Flex key={message.id}>{handleParse(message.content as string) ?? ''}</Flex>}
{message.role === 'assistant' && (
<div>
<div
style={{
width: 'calc(100% - 20px)',
marginLeft: '20px'
}}
>
<Flex>
{isLoading && !message.content && index === messages.length - 1 && (
<Flex>
<Spin indicator={<LoadingOutlined />} />
<span></span>
</Flex>
)}
{message.content ?? ''}
</Flex>
{message?.generativeUI?.()}
</div>
</div>
)}
</Flex>
)
})}
<div className='p-4'>
<Mentions
autoFocus
open={false}
placeholder='请输入内容'
rows={4}
value={newMessage}
maxLength={10000}
onKeyDown={handleKeyDown}
options={[]}
onInput={e => {
const value = (e.target as HTMLInputElement).value
// 检查内容是否只包含空格或回车符
if (/^\s*$/.test(value)) {
setNewMessage('') // 如果只包含空格或回车符,清空输入框
} else {
setNewMessage(value) // 否则更新输入内容
}
}}
/>
</div>
</>
)
}
const Test2: React.FC = () => {
return (
<CopilotKit
runtimeUrl='/agui-api/copilotkit/dify'
showDevConsole={false}
// publicApiKey={'ck_pub_cc922145a5da9b8513bc10df473cd6f7'}
agent='agentic_chat_metadata'
>
<Chat />
</CopilotKit>
)
}
export default Test2

View File

@@ -0,0 +1,216 @@
import { useState } from 'react'
import { Button, Card, Form, Input, message, Space } from 'antd'
import { PageHeader } from '@ant-design/pro-components'
import { translateChineseToEnglish } from '@/api/zh-en-translator'
const ZhEnTranslator = () => {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [translationResult, setTranslationResult] = useState('')
const [currentInput, setCurrentInput] = useState('')
const maxChars = 5000
const handleTranslate = async () => {
try {
const source_content = form.getFieldValue('source_content') || ''
if (!source_content.trim()) {
message.warning('请输入要翻译的中文内容')
return
}
setLoading(true)
setTranslationResult('')
const response = await translateChineseToEnglish(source_content)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`翻译请求失败: ${response.status} ${errorText}`)
}
if (!response.body) {
throw new Error('响应体为空')
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
let fullContent = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
if (trimmedLine === 'data: [DONE]') {
message.success('翻译完成')
setLoading(false)
return
}
continue
}
if (trimmedLine.startsWith('data: ')) {
try {
const data = trimmedLine.slice(6)
const parsed = JSON.parse(data)
if (parsed.event === 'message' && parsed.answer) {
fullContent += parsed.answer
setTranslationResult(fullContent)
} else if (parsed.event === 'error') {
throw new Error(parsed.message || 'Dify API 返回错误')
}
} catch (parseError) {
console.warn('跳过无法解析的行:', trimmedLine)
}
}
}
}
} finally {
reader.releaseLock()
}
if (fullContent) {
message.success('翻译完成')
} else {
throw new Error('未收到翻译结果')
}
} catch (error) {
console.error('翻译错误:', error)
message.error(error instanceof Error ? error.message : '翻译失败,请稍后重试')
} finally {
setLoading(false)
}
}
const handleClear = () => {
form.resetFields()
setTranslationResult('')
setCurrentInput('')
message.info('已清空内容')
}
const handleCopy = async () => {
if (!translationResult) {
message.warning('没有可复制的内容')
return
}
try {
await navigator.clipboard.writeText(translationResult)
message.success('复制成功')
} catch (error) {
console.error('复制失败:', error)
message.error('复制失败,请手动复制')
}
}
return (
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px' }}>
<PageHeader
title="中文转英文翻译助手"
subTitle="专业的中文到英文文本翻译工具,支持流式输出,翻译结果地道自然"
style={{ marginBottom: '30px' }}
/>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Card title="参数输入区">
<Form form={form} layout="vertical">
<Form.Item
name="source_content"
label={
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<span></span>
<span style={{ color: currentInput.length > maxChars ? '#ff4d4f' : '#999' }}>
{currentInput.length} / {maxChars}
</span>
</div>
}
rules={[
{ required: true, message: '请输入要翻译的中文内容' },
{ max: maxChars, message: `输入内容不能超过${maxChars}个字符` }
]}
>
<Input.TextArea
placeholder="请输入需要翻译的中文内容最多5000个字符"
rows={8}
value={currentInput}
onChange={(e) => setCurrentInput(e.target.value)}
showCount
maxLength={maxChars}
style={{ fontSize: '14px' }}
/>
</Form.Item>
</Form>
</Card>
<Space style={{ width: '100%', justifyContent: 'center' }} size="middle">
<Button
type="primary"
size="large"
onClick={handleTranslate}
loading={loading}
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
>
</Button>
<Button
size="large"
onClick={handleClear}
disabled={loading}
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
>
</Button>
</Space>
<Card
title="翻译结果"
extra={
translationResult && (
<Button onClick={handleCopy} size="small">
</Button>
)
}
>
<div
style={{
minHeight: '150px',
maxHeight: '400px',
overflowY: 'auto',
padding: '15px',
backgroundColor: '#fafafa',
border: '1px solid #d9d9d9',
borderRadius: '4px',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
fontSize: '14px',
lineHeight: '1.6'
}}
>
{loading ? (
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
...
</div>
) : translationResult || (
<div style={{ textAlign: 'center', color: '#999' }}>
</div>
)}
</div>
</Card>
</Space>
</div>
)
}
export default ZhEnTranslator

59
src/router/index.tsx Normal file
View File

@@ -0,0 +1,59 @@
import { Spin } from 'antd'
import { FC, ReactNode, Suspense, lazy } from 'react'
import { Navigate, RouteObject } from 'react-router-dom'
import { createHashRouter } from 'react-router-dom'
const Loading = () => (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh'
}}
>
<Spin size='large' />
</div>
)
export const LazyLoad = (Component: FC): ReactNode => {
return (
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
)
}
const router: RouteObject[] = [
{
path: '/',
children: [
{
path: '/',
element: LazyLoad(lazy(() => import('@/pages/home')))
},
{
path: '/test1',
element: LazyLoad(lazy(() => import('@/pages/test1')))
},
{
path: '/test2',
element: LazyLoad(lazy(() => import('@/pages/test2')))
},
{
path: '/zh-en-translator',
element: LazyLoad(lazy(() => import('@/pages/zh-en-translator')))
},
{
path: '/404',
element: <>404</>
}
]
},
{
path: '*',
element: <Navigate to='/404' />
}
]
export default createHashRouter(router)

43
src/utils/cacheUtil.ts Normal file
View File

@@ -0,0 +1,43 @@
export function cacheGet(key: string): string {
return localStorage.getItem(key) || ''
}
export function cacheSet(key: string, value: string) {
localStorage.setItem(key, value)
}
export function cacheClear(key: string) {
localStorage.removeItem(key)
}
export function cacheDeleteAll() {
localStorage.clear()
}
export interface UserInfo {
id?: string
avatar?: string
gender?: string
nickName?: string
username?: string
position?: string
deptName?: string
mobile?: string
bizMail?: string
email?: string
corpName?: string
effectivePoint?: string
totalPoint?: string
}
export function getUserInfo(): UserInfo | null {
const res = localStorage.getItem('userInfo')
if (!res) {
return null
} else {
return JSON.parse(res)
}
}
export function getToken() {
return localStorage.getItem('token')
}

8
src/utils/polyfills.ts Normal file
View File

@@ -0,0 +1,8 @@
if (!Object.hasOwn) {
Object.defineProperty(Object, 'hasOwn', {
value: function(obj: any, prop: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, prop)
},
configurable: true
})
}

2
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
declare module '@tailwindcss/vite'

5
tailwind.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: 'class',
plugins: []
}

29
tsconfig.app.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "src/api/.ts"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

28
tsconfig.node.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["vite.config.ts"]
}

33
vite.config.ts Normal file
View File

@@ -0,0 +1,33 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// https://vite.dev/config/
export default defineConfig(() => {
return {
base: './',
plugins: [tailwindcss(), react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
optimizeDeps: {
include: ['dayjs/locale/zh-cn']
},
server: {
host: '0.0.0.0',
allowedHosts: true as true,
cors: true,
open: true,
port: 5173
},
// 生产预览服务器配置 (vite preview) - 仅用于本地测试
preview: {
host: '0.0.0.0',
port: 3000,
cors: true
}
}
})