Initial commit: Copilot Toolbox template project
This commit is contained in:
44
src/App.tsx
Normal file
44
src/App.tsx
Normal 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
28
src/api/common.ts
Normal 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())
|
||||
}
|
||||
34
src/api/zh-en-translator.ts
Normal file
34
src/api/zh-en-translator.ts
Normal 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
29
src/index.css
Normal 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
12
src/main.tsx
Normal 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
57
src/pages/home/index.tsx
Normal 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
81
src/pages/test1/index.tsx
Normal 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
158
src/pages/test2/index.tsx
Normal 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
|
||||
216
src/pages/zh-en-translator/index.tsx
Normal file
216
src/pages/zh-en-translator/index.tsx
Normal 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
59
src/router/index.tsx
Normal 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
43
src/utils/cacheUtil.ts
Normal 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
8
src/utils/polyfills.ts
Normal 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
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '@tailwindcss/vite'
|
||||
Reference in New Issue
Block a user