Compare commits
No commits in common. "main" and "feature/project_dedtail" have entirely different histories.
main
...
feature/pr
|
@ -1,24 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"hash": "fe1d0f26",
|
||||
"configHash": "71314853",
|
||||
"lockfileHash": "2984ce1a",
|
||||
"browserHash": "72fdc116",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
{
|
||||
"hash": "fe1d0f26",
|
||||
"configHash": "71314853",
|
||||
"lockfileHash": "2984ce1a",
|
||||
"browserHash": "72fdc116",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
|
10
README.md
10
README.md
|
@ -1,5 +1,5 @@
|
|||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
|
|
156
eslint.config.ts
156
eslint.config.ts
|
@ -1,79 +1,77 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginVue from "eslint-plugin-vue";
|
||||
import markdown from "@eslint/markdown";
|
||||
import css from "@eslint/css";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
// 解决TypeScript-ESLint与ESLint 9.x类型不兼容问题
|
||||
{ settings: {} },
|
||||
// 基础配置
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,mts,cts,vue}"],
|
||||
plugins: { js },
|
||||
extends: ["js/recommended"],
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node },
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
|
||||
// TypeScript配置
|
||||
...tseslint.configs.recommended.map(config => ({
|
||||
...config,
|
||||
// 显式指定parser以解决类型不兼容问题
|
||||
languageOptions: { ...config.languageOptions, parser: tseslint.parser }
|
||||
})),
|
||||
|
||||
// Vue 3配置
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
|
||||
// Vue文件特定配置
|
||||
{
|
||||
files: ["**/*.vue"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Markdown配置
|
||||
{
|
||||
files: ["**/*.md"],
|
||||
plugins: { markdown },
|
||||
language: "markdown/commonmark",
|
||||
extends: ["markdown/recommended"]
|
||||
},
|
||||
|
||||
// CSS配置
|
||||
{
|
||||
files: ["**/*.css"],
|
||||
plugins: { css },
|
||||
language: "css/css",
|
||||
extends: ["css/recommended"]
|
||||
},
|
||||
|
||||
// 自定义规则
|
||||
{
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off', // 允许单字组件名
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'off', // 关闭未使用变量检查
|
||||
'@typescript-eslint/no-unused-vars-experimental': 'off', // 关闭实验性检查
|
||||
'vue/no-unused-vars': 'off', // 关闭Vue未使用变量检查
|
||||
'vue/no-v-model-argument': 'off', // 允许v-model参数
|
||||
'vue/no-async-in-computed-properties': 'off', // 禁用有问题的规则
|
||||
'vue/no-child-content': 'off'// 禁用另一个有问题的规则
|
||||
}
|
||||
},
|
||||
|
||||
// 忽略node_modules和dist目录
|
||||
{
|
||||
ignores: ['node_modules/**', 'dist/**', 'public/**', , 'src/components/chat/ChatAi.ts', 'src/components/chat/MockSSEResponse.ts', 'src/components/chat/ChatAi.vue', 'src/components/chat/ChatAi.tsx', 'src/components/chat/ChatAi.jsx', 'src/components/chat/ChatAi.js', 'src/components/chat/**', 'src/components/chat/ChatAi.js']
|
||||
}
|
||||
]);
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginVue from "eslint-plugin-vue";
|
||||
import markdown from "@eslint/markdown";
|
||||
import css from "@eslint/css";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
// 解决TypeScript-ESLint与ESLint 9.x类型不兼容问题
|
||||
{ settings: {} },
|
||||
// 基础配置
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,mts,cts,vue}"],
|
||||
plugins: { js },
|
||||
extends: ["js/recommended"],
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node },
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
|
||||
// TypeScript配置
|
||||
...tseslint.configs.recommended.map(config => ({
|
||||
...config,
|
||||
// 显式指定parser以解决类型不兼容问题
|
||||
languageOptions: { ...config.languageOptions, parser: tseslint.parser }
|
||||
})),
|
||||
|
||||
// Vue 3配置
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
|
||||
// Vue文件特定配置
|
||||
{
|
||||
files: ["**/*.vue"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Markdown配置
|
||||
{
|
||||
files: ["**/*.md"],
|
||||
plugins: { markdown },
|
||||
language: "markdown/commonmark",
|
||||
extends: ["markdown/recommended"]
|
||||
},
|
||||
|
||||
// CSS配置
|
||||
{
|
||||
files: ["**/*.css"],
|
||||
plugins: { css },
|
||||
language: "css/css",
|
||||
extends: ["css/recommended"]
|
||||
},
|
||||
|
||||
// 自定义规则
|
||||
{
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off', // 允许单字组件名
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'off', // 修改此处,允许声明变量未使用
|
||||
'vue/no-v-model-argument': 'off', // 允许v-model参数
|
||||
'vue/no-async-in-computed-properties': 'off', // 禁用有问题的规则
|
||||
'vue/no-child-content': 'off'// 禁用另一个有问题的规则
|
||||
}
|
||||
},
|
||||
|
||||
// 忽略node_modules和dist目录
|
||||
{
|
||||
ignores: ['node_modules/**', 'dist/**', 'public/**']
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/images/logo.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<!--<link rel="preload" as="image" href="/src/assets/images/hero-bg.jpg" fetchpriority="high">-->
|
||||
<link rel="preload" as="image" href="images/logo.png" fetchpriority="high">
|
||||
<link rel="preload" as="image" href="images/hero-bg.jpg" fetchpriority="high">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>优阅工作室</title>
|
||||
<title>Vite + Vue + TS</title>
|
||||
|
||||
|
||||
</head>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
|
@ -1,31 +1,28 @@
|
|||
{
|
||||
"name": "company_website",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tdesign-vue-next/chat": "^0.4.5",
|
||||
"@types/node": "^24.2.1",
|
||||
"@vue/language-core": "^3.0.5",
|
||||
"axios": "^1.11.0",
|
||||
"pinia": "^3.0.3",
|
||||
"tdesign-vue-next": "^1.15.2",
|
||||
"vue": "^3.5.18",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"less": "^4.4.1",
|
||||
"less-loader": "^12.3.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.2",
|
||||
"vue-tsc": "^3.0.5"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "company_website",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^24.2.1",
|
||||
"@vue/language-core": "^3.0.5",
|
||||
"axios": "^1.11.0",
|
||||
"pinia": "^3.0.3",
|
||||
"tdesign-vue-next": "^1.15.2",
|
||||
"vue": "^3.5.18",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.2",
|
||||
"vue-tsc": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
|
342
src/App.vue
342
src/App.vue
|
@ -1,172 +1,170 @@
|
|||
<!-- src/App.vue -->
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- 公共组件:当 hideHeader 为 false 时显示 -->
|
||||
<Navbar v-if="!route.meta.hideHeader" />
|
||||
<BackToTop v-if="!route.meta.hideHeader" />
|
||||
|
||||
<!-- 路由出口:根据当前路由渲染对应组件 -->
|
||||
<!-- 首页路由渲染 HomePage.vue,详情页路由渲染 Project3Zh.vue 等 -->
|
||||
<router-view />
|
||||
|
||||
<!-- 公共页脚:当 hideHeader 为 false 时显示 -->
|
||||
<footer class="footer" v-if="!route.meta.hideHeader">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<p>© {{ new Date().getFullYear() }} Company Name. All rights reserved.</p>
|
||||
<div class="social-links">
|
||||
<!-- 社交图标代码不变 -->
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg></a>
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg></a>
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg></a>
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BackToTop from './components/common/BackToTop.vue'
|
||||
import Navbar from './components/common/Navbar.vue'
|
||||
import { useLanguageStore } from './store/language'
|
||||
import { provide } from 'vue'
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
|
||||
|
||||
// 初始化语言store并提供翻译函数
|
||||
const languageStore = useLanguageStore()
|
||||
provide('t', (key: string) => languageStore.t(key))
|
||||
|
||||
// 获取当前路由
|
||||
const route = useRoute();
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(90deg, white, var(--primary-light));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 0.8rem 2rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: white;
|
||||
color: var(--bg-color);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #1a1a2e;
|
||||
color: white;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.social-links a {
|
||||
color: white;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.social-links a:hover {
|
||||
color: #4cc9f0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-content h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- src/App.vue -->
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- 公共组件:当 hideHeader 为 false 时显示 -->
|
||||
<Navbar v-if="!route.meta.hideHeader" />
|
||||
<BackToTop v-if="!route.meta.hideHeader" />
|
||||
|
||||
<!-- 路由出口:根据当前路由渲染对应组件 -->
|
||||
<!-- 首页路由渲染 HomePage.vue,详情页路由渲染 Project3Zh.vue 等 -->
|
||||
<router-view />
|
||||
|
||||
<!-- 公共页脚:当 hideHeader 为 false 时显示 -->
|
||||
<footer class="footer" v-if="!route.meta.hideHeader">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<p>© {{ new Date().getFullYear() }} Company Name. All rights reserved.</p>
|
||||
<div class="social-links">
|
||||
<!-- 社交图标代码不变 -->
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg></a>
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg></a>
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg></a>
|
||||
<a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BackToTop from './components/common/BackToTop.vue'
|
||||
import Navbar from './components/common/Navbar.vue'
|
||||
import { useLanguageStore } from './store/language'
|
||||
import { provide } from 'vue'
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
// 初始化语言store并提供翻译函数
|
||||
const languageStore = useLanguageStore()
|
||||
provide('t', (key: string) => languageStore.t(key))
|
||||
|
||||
// 获取当前路由
|
||||
const route = useRoute();
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(90deg, white, var(--primary-light));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 0.8rem 2rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: white;
|
||||
color: var(--bg-color);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #1a1a2e;
|
||||
color: white;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.social-links a {
|
||||
color: white;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.social-links a:hover {
|
||||
color: #4cc9f0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-content h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.6 KiB |
|
@ -1,41 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,291 +0,0 @@
|
|||
// 定义类型接口
|
||||
interface FetchSSEOptions {
|
||||
success?: (data: any) => void;
|
||||
fail?: () => void;
|
||||
complete?: (isOk: boolean, msg?: string) => void;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface SSEEvent {
|
||||
type: string | null;
|
||||
data: any;
|
||||
}
|
||||
|
||||
const Api = {
|
||||
// CHAT_AI_URL: 'http://localhost:8180/chat/generateStreamFlex'
|
||||
CHAT_AI_URL: '/rag/query_rag'
|
||||
};
|
||||
import axios from 'axios'
|
||||
const getAi=(msg: any):any => {
|
||||
return axios.get(Api.CHAT_AI_URL, {
|
||||
params: { message: msg },
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
responseType: 'stream'
|
||||
});
|
||||
}
|
||||
export class MockSSEResponse {
|
||||
private controller!: ReadableStreamDefaultController<Uint8Array>;
|
||||
|
||||
private encoder = new TextEncoder();
|
||||
|
||||
private stream: ReadableStream<Uint8Array>;
|
||||
|
||||
private error: boolean;
|
||||
|
||||
private currentPhase: 'reasoning' | 'content' = 'reasoning';
|
||||
|
||||
private data: {
|
||||
reasoning: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
private delay: number;
|
||||
|
||||
constructor(
|
||||
data: {
|
||||
reasoning: string; // 推理内容
|
||||
content: string; // 正式内容
|
||||
},
|
||||
delay: number = 100,
|
||||
error = false,
|
||||
) {
|
||||
this.data = data;
|
||||
this.delay = delay;
|
||||
this.error = error;
|
||||
|
||||
this.stream = new ReadableStream({
|
||||
start: (controller) => {
|
||||
this.controller = controller;
|
||||
if (!this.error) {
|
||||
// 如果不是错误情况,则开始推送数据
|
||||
setTimeout(() => this.pushData(), this.delay); // 延迟开始推送数据
|
||||
}
|
||||
},
|
||||
cancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
private pushData() {
|
||||
try {
|
||||
if (this.currentPhase === 'reasoning') {
|
||||
// 推送推理内容
|
||||
if (this.data.reasoning.length > 0) {
|
||||
const chunk = JSON.stringify({
|
||||
delta: {
|
||||
reasoning_content: this.data.reasoning.slice(0, 1),
|
||||
content: '',
|
||||
},
|
||||
finished: false,
|
||||
});
|
||||
this.controller.enqueue(this.encoder.encode(chunk));
|
||||
this.data.reasoning = this.data.reasoning.slice(1);
|
||||
// 设置下次推送
|
||||
setTimeout(() => this.pushData(), this.delay);
|
||||
} else {
|
||||
// 推理内容推送完成,切换到正式内容
|
||||
this.currentPhase = 'content';
|
||||
setTimeout(() => this.pushData(), this.delay); // 立即开始推送正式内容
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentPhase === 'content') {
|
||||
// 推送正式内容
|
||||
if (this.data.content.length > 0) {
|
||||
const chunk = JSON.stringify({
|
||||
delta: {
|
||||
reasoning_content: '',
|
||||
content: this.data.content.slice(0, 1),
|
||||
},
|
||||
finished: this.data.content.length === 1, // 最后一个字符时标记完成
|
||||
});
|
||||
this.controller.enqueue(this.encoder.encode(chunk));
|
||||
this.data.content = this.data.content.slice(1);
|
||||
|
||||
// 设置下次推送
|
||||
setTimeout(() => this.pushData(), this.delay);
|
||||
} else {
|
||||
// const finalPayload = JSON.stringify({
|
||||
// delta: {
|
||||
// reasoning_content: '',
|
||||
// content: '',
|
||||
// },
|
||||
// finished: true,
|
||||
// });
|
||||
// this.controller.enqueue(this.encoder.encode(`${finalPayload}`));
|
||||
// 全部内容推送完成
|
||||
setTimeout(() => this.controller.close(), this.delay);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
getResponse(_msg: String): Promise<Response> {
|
||||
return new Promise((resolve) => {
|
||||
resolve(getAi(_msg));
|
||||
});
|
||||
}
|
||||
|
||||
// 封装SSE连接 - 添加降级处理
|
||||
connectSSE = (url: string, params: any, onMessage?: (data: string) => void, onError?: (error: Event) => void) => {
|
||||
try {
|
||||
// 构建带参数的URL
|
||||
const queryString = Object.keys(params)
|
||||
.map((key: string) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key]?.message || params[key])}`)
|
||||
.join('&');
|
||||
|
||||
// const API_BASE_URL = 'http://localhost:8080';
|
||||
const API_BASE_URL = '';
|
||||
const fullUrl = `${API_BASE_URL}${url}?${queryString}`;
|
||||
|
||||
// 检查 EventSource 是否可用
|
||||
if (typeof EventSource === 'undefined') {
|
||||
throw new Error('EventSource not supported');
|
||||
}
|
||||
|
||||
// 创建EventSource
|
||||
const eventSource = new EventSource(fullUrl);
|
||||
|
||||
eventSource.onmessage = (event: MessageEvent) => {
|
||||
const { data } = event;
|
||||
if (onMessage) {
|
||||
if (data === '[DONE]') {
|
||||
onMessage('[DONE]');
|
||||
} else {
|
||||
onMessage(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error: Event) => {
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
return eventSource;
|
||||
} catch (error) {
|
||||
// 降级处理:使用模拟数据
|
||||
console.warn('EventSource 不可用,使用模拟数据');
|
||||
|
||||
// 模拟 SSE 行为
|
||||
const mockData = [
|
||||
"您好!",
|
||||
"我是AI助手。",
|
||||
"正在处理您的问题...",
|
||||
"根据分析,",
|
||||
"这是详细的回答。",
|
||||
"希望能帮到您!",
|
||||
"[DONE]"
|
||||
];
|
||||
|
||||
let index = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (index < mockData.length) {
|
||||
if (onMessage) onMessage(mockData[index]);
|
||||
index++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// 返回一个可以关闭的模拟对象
|
||||
return {
|
||||
close: () => clearInterval(interval),
|
||||
readyState: 1
|
||||
} as EventSource;
|
||||
}
|
||||
};
|
||||
|
||||
// AI超级智能体聊天 - 实际使用 connectSSE 方法
|
||||
chatWithManus = (message: any, onMessage?: (data: string) => void, onError?: (error: Event) => void) => {
|
||||
return this.connectSSE('/rag/query_rag', { message }, onMessage, onError);
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchSSE = async (options: FetchSSEOptions = {}) => {
|
||||
const { success, fail, complete, url } = options;
|
||||
// fetch请求流式接口url,需传入接口url和参数
|
||||
if (!url) {
|
||||
complete?.(false, 'URL is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const responsePromise = fetch(url).catch((e: any) => {
|
||||
const msg = e.toString() || '流式接口异常';
|
||||
complete?.(false, msg);
|
||||
return Promise.reject(e);
|
||||
});
|
||||
|
||||
responsePromise
|
||||
.then((response) => {
|
||||
if (!response?.ok) {
|
||||
complete?.(false, response.statusText);
|
||||
fail?.();
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
if (!reader) {
|
||||
complete?.(false, 'No reader available');
|
||||
return;
|
||||
}
|
||||
|
||||
const bufferArr: string[] = [];
|
||||
let dataText = '';
|
||||
const event: SSEEvent = { type: null, data: null };
|
||||
|
||||
async function processText({ done, value }: ReadableStreamReadResult<Uint8Array>): Promise<void> {
|
||||
if (done) {
|
||||
complete?.(true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
const chunk = decoder.decode(value);
|
||||
const buffers = chunk.toString().replace(/ /g, ' ').split(/\r?\n/);
|
||||
bufferArr.push(...buffers);
|
||||
let i = 0;
|
||||
while (i < bufferArr.length) {
|
||||
const line = bufferArr[i];
|
||||
if (line) {
|
||||
dataText += line;
|
||||
const response = line.slice(5);
|
||||
if (response === '[DONE]') {
|
||||
event.type = 'finish';
|
||||
dataText = '';
|
||||
} else {
|
||||
event.type = 'delta';
|
||||
event.data = response;
|
||||
|
||||
// const choices = JSON.parse(response.trim())?.choices?.[0];
|
||||
// if (choices.finish_reason === 'stop') {
|
||||
// event.type = 'finish';
|
||||
// dataText = '';
|
||||
// } else {
|
||||
// event.type = 'delta';
|
||||
// event.data = choices;
|
||||
// }
|
||||
}
|
||||
}
|
||||
if (event.type && event.data) {
|
||||
try {
|
||||
const jsonData = JSON.parse(event.data);
|
||||
success?.(jsonData);
|
||||
} catch (e) {
|
||||
success?.(event.data);
|
||||
}
|
||||
event.type = null;
|
||||
event.data = null;
|
||||
}
|
||||
bufferArr.splice(i, 1);
|
||||
}
|
||||
return reader!.read().then(processText);
|
||||
}
|
||||
|
||||
return reader.read().then(processText);
|
||||
})
|
||||
.catch(() => {
|
||||
// 处理整个链式调用过程中发生的任何错误
|
||||
fail?.();
|
||||
});
|
||||
};
|
|
@ -1,435 +0,0 @@
|
|||
<template>
|
||||
<t-space v-if="isVisible1" align="center" @click="visibleModelessDrag = true" class="ai_style" >
|
||||
<img :src="aiImg" alt="勤怠登录页面" width="36px" height="36px">
|
||||
</t-space>
|
||||
<t-config-provider :global-config="globalConfig">
|
||||
<t-dialog
|
||||
v-model:visible="visibleModelessDrag"
|
||||
:footer="false"
|
||||
id ="abc"
|
||||
:header="t('ai.title')"
|
||||
mode="modeless"
|
||||
showOverlay="true"
|
||||
|
||||
draggable
|
||||
:on-confirm="() => (visibleModelessDrag = false)"
|
||||
>
|
||||
<template #body>
|
||||
<t-chat
|
||||
layout="both"
|
||||
style="height: 600px;"
|
||||
:z-index="3000"
|
||||
:data="chatList"
|
||||
:clear-history="chatList.length > 0 && !isStreamLoad"
|
||||
:text-loading="loading"
|
||||
:is-stream-load="isStreamLoad"
|
||||
@on-action="operation"
|
||||
@clear="clearConfirm"
|
||||
>
|
||||
<template #actions="{ item, index }">
|
||||
<t-chat-action
|
||||
:content="item.content"
|
||||
:operation-btn="['good', 'bad', 'replay', 'copy']"
|
||||
@operation="handleOperation"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<t-chat-input v-model="inputValue" :stop-disabled="isStreamLoad" @send="inputEnter" @stop="onStop"> </t-chat-input>
|
||||
</template>
|
||||
</t-chat>
|
||||
|
||||
|
||||
</template>
|
||||
</t-dialog>
|
||||
</t-config-provider>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, inject, watch, computed } from 'vue'
|
||||
import aiImg from '@/assets/ai_img.png'
|
||||
import { useLanguageStore } from '../../store/language'
|
||||
import { MockSSEResponse } from './ChatAi';
|
||||
import { globalConfig } from '../../locales/globalConfig'
|
||||
|
||||
const visibleModelessDrag = ref(false);
|
||||
|
||||
const fetchCancel = ref<any>(null);
|
||||
const loading = ref(false);
|
||||
const isStreamLoad = ref(false);
|
||||
const chatRef = ref<any>(null);
|
||||
const isShowToBottom = ref(false);
|
||||
const inputValue = ref('');
|
||||
|
||||
const store = useLanguageStore()
|
||||
|
||||
// 控制按钮显示/隐藏的状态
|
||||
const isVisible1 = ref(false)
|
||||
|
||||
const handleScroll1 = () => {
|
||||
isVisible1.value = window.scrollY > 300
|
||||
};
|
||||
|
||||
// 注入翻译函数
|
||||
const t = inject<(key: string) => string>('t') || ((key: string) => key)
|
||||
|
||||
// 挂载时添加滚动监听
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll1)
|
||||
})
|
||||
|
||||
// 卸载时移除滚动监听和关闭EventSource
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll1)
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
})
|
||||
|
||||
// 计算属性,根据当前语言返回对应的文本
|
||||
const clearHistoryBtnText = computed(() => {
|
||||
return t('ai.clearHistory')
|
||||
})
|
||||
|
||||
// 获取指定格式的时间字符串
|
||||
const getFormattedDateTime = (): string => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear().toString().slice(-2);
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = now.getDate().toString().padStart(2, '0');
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `[${year}-${month}-${day} ${hours}:${minutes}]`;
|
||||
};
|
||||
|
||||
// 倒序渲染
|
||||
const chatList = ref<any[]>([
|
||||
{
|
||||
avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png',
|
||||
name: 'youyueAI',
|
||||
datetime: getFormattedDateTime(),
|
||||
content: t('ai.initContent'),
|
||||
role: 'assistant',
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
const handleOperation = function (type: string, options: any) {
|
||||
console.log('handleOperation', type, options);
|
||||
};
|
||||
const operation = function (type: string, options: any) {
|
||||
console.log(type, options);
|
||||
};
|
||||
const clearConfirm = function () {
|
||||
chatList.value = [];
|
||||
};
|
||||
const onStop = function () {
|
||||
if (fetchCancel.value) {
|
||||
fetchCancel.value.controller.close();
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
const inputEnter = function (inputValue: string) {
|
||||
if (isStreamLoad.value) {
|
||||
return;
|
||||
}
|
||||
if (!inputValue) return;
|
||||
const params = {
|
||||
avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
|
||||
name: '自己',
|
||||
datetime: new Date().toDateString(),
|
||||
content: inputValue,
|
||||
role: 'user',
|
||||
};
|
||||
chatList.value.unshift(params);
|
||||
// 空消息占位
|
||||
const params2 = {
|
||||
avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png',
|
||||
name: 'youyueAI',
|
||||
datetime: new Date().toDateString(),
|
||||
content: '',
|
||||
role: 'assistant',
|
||||
};
|
||||
chatList.value.unshift(params2);
|
||||
handleData(inputValue);
|
||||
};
|
||||
|
||||
const fetchSSE = async (fetchFn: any, options: any) => {
|
||||
const response = await fetchFn();
|
||||
const { success, fail, complete } = options;
|
||||
if (response.status !== 200) {
|
||||
complete?.(false, response.statusText);
|
||||
fail?.();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义类型接口
|
||||
interface ChatMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
reasoning?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
// 配置参数
|
||||
const config = {
|
||||
bufferSize: 5,
|
||||
flushInterval: 10,
|
||||
typingSpeed: 50,
|
||||
};
|
||||
|
||||
// 使用所有变量
|
||||
const displayText = ref('');
|
||||
const fullText = ref('');
|
||||
const isLoading = ref(true);
|
||||
const eventSource = ref<any>(null);
|
||||
|
||||
// 实际使用的变量
|
||||
let animationFrameId: number | null = null;
|
||||
const messageBuffer: string[] = [];
|
||||
let isProcessing = false;
|
||||
|
||||
// 处理消息缓冲区
|
||||
const processBuffer = async () => {
|
||||
isProcessing = true;
|
||||
const processNextMessage = () => {
|
||||
if (messageBuffer.length === 0) {
|
||||
isProcessing = false;
|
||||
return;
|
||||
}
|
||||
const message = messageBuffer.shift();
|
||||
if (message) {
|
||||
typeCharacter(message);
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(processNextMessage);
|
||||
};
|
||||
animationFrameId = requestAnimationFrame(processNextMessage);
|
||||
};
|
||||
|
||||
// 模拟打字效果
|
||||
const typeCharacter = (content: string) => {
|
||||
const index = 0; // 实际使用的索引
|
||||
setTimeout(() => {
|
||||
if (chatList.value && chatList.value[0]) {
|
||||
chatList.value[0].content += content.replace(/ /g, ' ');
|
||||
}
|
||||
}, config.typingSpeed);
|
||||
};
|
||||
|
||||
// 实际使用 connectSSE 的数据处理函数
|
||||
const handleData = async (inputVal?: string) => {
|
||||
loading.value = true;
|
||||
isStreamLoad.value = true;
|
||||
|
||||
// 使用 MockSSEResponse 的 connectSSE 方法
|
||||
const mockSSE = new MockSSEResponse({
|
||||
reasoning: "正在分析问题...",
|
||||
content: "正在连接服务器..."
|
||||
});
|
||||
|
||||
// // 实际使用 connectSSE 方法
|
||||
// eventSource.value = mockSSE.chatWithManus(
|
||||
// inputVal,
|
||||
// (data: string) => {
|
||||
// // 处理接收到的消息
|
||||
// if (data === '[DONE]') {
|
||||
// loading.value = false;
|
||||
// isStreamLoad.value = false;
|
||||
// } else {
|
||||
// // 将接收到的数据添加到消息中
|
||||
// if (chatList.value && chatList.value[0]) {
|
||||
// // 如果是第一条消息,先清空再添加
|
||||
// if (chatList.value[0].content === '' || chatList.value[0].content === '正在连接服务器...') {
|
||||
// chatList.value[0].content = data;
|
||||
// } else {
|
||||
// chatList.value[0].content += data;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// (error: Event) => {
|
||||
// // console.error('SSE Error:', error);
|
||||
// loading.value = false;
|
||||
// isStreamLoad.value = false;
|
||||
// // if (chatList.value && chatList.value[0]) {
|
||||
// // chatList.value[0].content = '正在使用本地AI助手...';
|
||||
// // // 添加一个模拟的AI回复
|
||||
// // setTimeout(() => {
|
||||
// // if (chatList.value && chatList.value[0]) {
|
||||
// // chatList.value[0].content = `您好!我是AI助手。关于您的问题「${inputVal}」,我可以为您提供以下帮助:\n\n` +
|
||||
// // `1. 这是一个基于前端模拟的AI回复,因为后端服务暂时无法连接。\n` +
|
||||
// // `2. 在实际部署中,这里会连接到真正的AI服务。\n` +
|
||||
// // `3. 您可以继续提问,我会继续为您提供模拟的回复。\n\n` +
|
||||
// // `感谢您的理解!`;
|
||||
// // }
|
||||
// // }, 1000);
|
||||
// // }
|
||||
// }
|
||||
// );
|
||||
|
||||
const message = {
|
||||
message: inputValue.value,
|
||||
};
|
||||
const eventSource = mockSSE.chatWithManus(message);
|
||||
eventSource.onmessage = (event) => {
|
||||
const { data } = event;
|
||||
|
||||
if (data && data !== '[DONE]') {
|
||||
// messageBuffer.push(data);
|
||||
|
||||
loading.value = false;
|
||||
// 控制终止按钮
|
||||
isStreamLoad.value = false;
|
||||
// lastItem.reasoning += data.replaceAll(' ', ' ');
|
||||
// lastItem.content += data.replaceAll(' ', ' ');
|
||||
|
||||
// 将消息添加到缓冲区
|
||||
messageBuffer.push(data.replaceAll(' ', ' '));
|
||||
|
||||
// 如果不在处理中,则开始处理缓冲区
|
||||
if (!isProcessing) {
|
||||
processBuffer();
|
||||
}
|
||||
|
||||
// // 检查是否应该创建新气泡
|
||||
// const combinedText = messageBuffer.join('');
|
||||
|
||||
// // 句子结束或消息长度达到阈值
|
||||
// const lastChar = data.charAt(data.length - 1);
|
||||
// const hasCompleteSentence = chineseEndPunctuation.includes(lastChar) || data.includes('\n\n');
|
||||
// const isLongEnough = combinedText.length > 40;
|
||||
|
||||
// if (hasCompleteSentence || isLongEnough) {
|
||||
// createBubble(combinedText);
|
||||
// }
|
||||
}
|
||||
|
||||
if (data === '[DONE]') {
|
||||
// 如果还有未显示的内容,创建最后一个气泡
|
||||
// if (messageBuffer.length > 0) {
|
||||
// const remainingContent = messageBuffer.join('');
|
||||
// createBubble(remainingContent, 'ai-final');
|
||||
// }
|
||||
// 控制终止按钮
|
||||
isStreamLoad.value = false;
|
||||
loading.value = false;
|
||||
|
||||
// 完成后关闭连接
|
||||
// connectionStatus.value = 'disconnected';
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听SSE错误
|
||||
eventSource.onerror = (error) => {
|
||||
// 控制终止按钮
|
||||
isStreamLoad.value = false;
|
||||
loading.value = false;
|
||||
eventSource.close();
|
||||
// console.error('SSE Error:', error);
|
||||
// connectionStatus.value = 'error';
|
||||
// eventSource.close();
|
||||
// // 如果出错时有未显示的内容,也创建气泡
|
||||
// if (messageBuffer.length > 0) {
|
||||
// const remainingContent = messageBuffer.join('');
|
||||
// createBubble(remainingContent, 'ai-error');
|
||||
// }
|
||||
};
|
||||
|
||||
// 流结束时触发
|
||||
// eventSource.close = () => {
|
||||
// // 可在此处做清理工作,如关闭 EventSource
|
||||
// eventSource.close();
|
||||
// };
|
||||
};
|
||||
|
||||
// 修复 watch 中的类型问题
|
||||
watch(
|
||||
() => store.currentLanguage,
|
||||
(newLang: string, oldLang: string) => {
|
||||
// 使用 newLang 和 oldLang
|
||||
console.log('Language changed from', oldLang, 'to', newLang);
|
||||
chatList.value[0].content = t('ai.initContent');
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
/* 应用滚动条样式 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--td-scrollbar-color);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:horizontal:hover {
|
||||
background-color: var(--td-scrollbar-hover-color);
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--td-scroll-track-color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 第一种写法重写 组件样式
|
||||
:global(.t-dialog__ctx.t-dialog__ctx--modeless .t-dialog) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
:deep(.t-chat__text) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
// 第二种重写样式 定义一个啥也不带的 style
|
||||
<style>
|
||||
|
||||
.t-chat__actions-margin {
|
||||
display: none;
|
||||
}
|
||||
.t-dialog__ctx .t-dialog__wrap {
|
||||
z-index: 3001;
|
||||
right: 10;
|
||||
}
|
||||
|
||||
.t-dialog__ctx .t-dialog__position {
|
||||
justify-content: right;
|
||||
/* left: 86px; */
|
||||
position: fixed;
|
||||
right: 100px;
|
||||
bottom: 52px;
|
||||
}
|
||||
|
||||
.ai_style{
|
||||
gap: 16px;
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
right: 2rem;
|
||||
background-color: #d7e6ed;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.ai_style:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.t-dialog__ctx .t-dialog__position {
|
||||
justify-content: center;
|
||||
/* left: 86px; */
|
||||
position: unset;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,76 +1,75 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 控制按钮显示/隐藏的状态
|
||||
const isVisible = ref(false)
|
||||
|
||||
// 滚动监听函数
|
||||
const handleScroll = () => {
|
||||
// 当滚动超过300px时显示按钮
|
||||
isVisible.value = window.scrollY > 300
|
||||
|
||||
}
|
||||
|
||||
// 回到顶部函数
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
// 挂载时添加滚动监听
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
// 卸载时移除滚动监听
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="back-to-top"
|
||||
v-if="isVisible"
|
||||
@click="scrollToTop"
|
||||
aria-label="回到顶部"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.back-to-top svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 控制按钮显示/隐藏的状态
|
||||
const isVisible = ref(false)
|
||||
|
||||
// 滚动监听函数
|
||||
const handleScroll = () => {
|
||||
// 当滚动超过300px时显示按钮
|
||||
isVisible.value = window.scrollY > 300
|
||||
}
|
||||
|
||||
// 回到顶部函数
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
// 挂载时添加滚动监听
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
// 卸载时移除滚动监听
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="back-to-top"
|
||||
v-if="isVisible"
|
||||
@click="scrollToTop"
|
||||
aria-label="回到顶部"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.back-to-top svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,295 +1,295 @@
|
|||
<template>
|
||||
<section id="contact" class="contact-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">{{ t('contact.title') }}</h2>
|
||||
<div class="contact-container">
|
||||
<div class="contact-form">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="form-group">
|
||||
<label for="name">{{ t('contact.name') }}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
v-model="formData.name"
|
||||
required
|
||||
:placeholder="t('contact.name')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ t('contact.email') }}</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="formData.email"
|
||||
required
|
||||
:placeholder="t('contact.email')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subject">{{ t('contact.subject') }}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
v-model="formData.subject"
|
||||
required
|
||||
:placeholder="t('contact.subject')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">{{ t('contact.message') }}</label>
|
||||
<textarea
|
||||
id="message"
|
||||
v-model="formData.message"
|
||||
required
|
||||
rows="5"
|
||||
:placeholder="t('contact.message')"
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="submit-btn">{{ t('contact.send') }}</button>
|
||||
</form>
|
||||
<div v-if="showSuccessMessage" class="success-message">{{ t('contact.success') }}</div>
|
||||
<div v-if="showErrorMessage" class="error-message">{{ t('contact.error') }}</div>
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||
<polyline points="22,6 12,13 2,6"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<h3>Email</h3>
|
||||
<p>2187434671@qq.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<h3>Phone</h3>
|
||||
<p>(86)15668601400</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="9" width="18" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M7 10v3"></path>
|
||||
<path d="M17 10v3"></path>
|
||||
<path d="M9 22V12h6v10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<h3>Address</h3>
|
||||
<p>No. 28, Dayou Tianyuan, Hi-tech Industrial Park, Dalian, Liaoning Province, China</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
import { EmailService } from '../../services/emailService'
|
||||
|
||||
|
||||
export interface FormData {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
}
|
||||
|
||||
const formData = ref<FormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const showSuccessMessage = ref(false)
|
||||
const showErrorMessage = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
// 注入翻译函数
|
||||
const t = inject<(key: string) => string>('t') || ((key) => key)
|
||||
|
||||
const submitForm = async () => {
|
||||
isSubmitting.value = true
|
||||
showSuccessMessage.value = false
|
||||
showErrorMessage.value = false
|
||||
|
||||
try {
|
||||
const response = await EmailService.sendEmail(formData.value)
|
||||
|
||||
if (response.success) {
|
||||
showSuccessMessage.value = true
|
||||
// 重置表单
|
||||
formData.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
}
|
||||
// 5秒后隐藏成功消息
|
||||
setTimeout(() => {
|
||||
showSuccessMessage.value = false
|
||||
}, 5000)
|
||||
} else {
|
||||
showErrorMessage.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error)
|
||||
showErrorMessage.value = true
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-section {
|
||||
padding: 5rem 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
display: flex;
|
||||
gap: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
background-color: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #1a1a2e;
|
||||
font-weight: bold;
|
||||
text-align: left; /*label文字居左 */
|
||||
display: block; /* 确保标签独占一行,更好地控制对齐 */
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #4cc9f0;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: #4361ee;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 2rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/*email,phone,adress左对齐 */
|
||||
.info-item .info-text {
|
||||
text-align: left;
|
||||
}
|
||||
/* email,phone,adress的图标和内容整体向下移动 */
|
||||
.contact-info {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.info-icon {
|
||||
color: #4cc9f0;
|
||||
margin-right: 1rem;
|
||||
padding-top: 0.3rem;
|
||||
}
|
||||
|
||||
.info-text h3 {
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-text p {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: calc(100% - 1.6rem);
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
<template>
|
||||
<section id="contact" class="contact-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">{{ t('contact.title') }}</h2>
|
||||
<div class="contact-container">
|
||||
<div class="contact-form">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="form-group">
|
||||
<label for="name">{{ t('contact.name') }}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
v-model="formData.name"
|
||||
required
|
||||
:placeholder="t('contact.name')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ t('contact.email') }}</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="formData.email"
|
||||
required
|
||||
:placeholder="t('contact.email')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subject">{{ t('contact.subject') }}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
v-model="formData.subject"
|
||||
required
|
||||
:placeholder="t('contact.subject')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">{{ t('contact.message') }}</label>
|
||||
<textarea
|
||||
id="message"
|
||||
v-model="formData.message"
|
||||
required
|
||||
rows="5"
|
||||
:placeholder="t('contact.message')"
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="submit-btn">{{ t('contact.send') }}</button>
|
||||
</form>
|
||||
<div v-if="showSuccessMessage" class="success-message">{{ t('contact.success') }}</div>
|
||||
<div v-if="showErrorMessage" class="error-message">{{ t('contact.error') }}</div>
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||
<polyline points="22,6 12,13 2,6"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<h3>Email</h3>
|
||||
<p>2187434671@qq.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<h3>Phone</h3>
|
||||
<p>(86)15668601400</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="9" width="18" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M7 10v3"></path>
|
||||
<path d="M17 10v3"></path>
|
||||
<path d="M9 22V12h6v10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<h3>Address</h3>
|
||||
<p>No. 28, Dayou Tianyuan, Hi-tech Industrial Park, Dalian, Liaoning Province, China</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
import { EmailService } from '../../services/emailService'
|
||||
|
||||
|
||||
export interface FormData {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
}
|
||||
|
||||
const formData = ref<FormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const showSuccessMessage = ref(false)
|
||||
const showErrorMessage = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
// 注入翻译函数
|
||||
const t = inject<(key: string) => string>('t') || ((key) => key)
|
||||
|
||||
const submitForm = async () => {
|
||||
isSubmitting.value = true
|
||||
showSuccessMessage.value = false
|
||||
showErrorMessage.value = false
|
||||
|
||||
try {
|
||||
const response = await EmailService.sendEmail(formData.value)
|
||||
|
||||
if (response.success) {
|
||||
showSuccessMessage.value = true
|
||||
// 重置表单
|
||||
formData.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
}
|
||||
// 5秒后隐藏成功消息
|
||||
setTimeout(() => {
|
||||
showSuccessMessage.value = false
|
||||
}, 5000)
|
||||
} else {
|
||||
showErrorMessage.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error)
|
||||
showErrorMessage.value = true
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-section {
|
||||
padding: 5rem 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
display: flex;
|
||||
gap: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
background-color: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #1a1a2e;
|
||||
font-weight: bold;
|
||||
text-align: left; /*label文字居左 */
|
||||
display: block; /* 确保标签独占一行,更好地控制对齐 */
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #4cc9f0;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: #4361ee;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 2rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/*email,phone,adress左对齐 */
|
||||
.info-item .info-text {
|
||||
text-align: left;
|
||||
}
|
||||
/* email,phone,adress的图标和内容整体向下移动 */
|
||||
.contact-info {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.info-icon {
|
||||
color: #4cc9f0;
|
||||
margin-right: 1rem;
|
||||
padding-top: 0.3rem;
|
||||
}
|
||||
|
||||
.info-text h3 {
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-text p {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: calc(100% - 1.6rem);
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
</style>
|
|
@ -1,175 +1,175 @@
|
|||
<template>
|
||||
<section id="team" class="team-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">{{ t('team.title') }}</h2>
|
||||
<div class="team-grid">
|
||||
<div v-for="member in teamMembers" :key="member.id" class="team-card">
|
||||
<div class="team-photo">
|
||||
<img :src="member.photo" :alt="t(member.nameKey)" />
|
||||
</div>
|
||||
<div class="team-info">
|
||||
<h3>{{ t(member.nameKey) }}</h3>
|
||||
<p class="role">{{ t('team.memberRole') }}: {{ t(member.roleKey) }}</p>
|
||||
<p class="bio">{{ t('team.memberBio') }}: {{ t(member.bioKey) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
|
||||
export interface TeamMember {
|
||||
id: number
|
||||
nameKey: string
|
||||
roleKey: string
|
||||
bioKey: string
|
||||
photo: string
|
||||
}
|
||||
|
||||
// 批量导入 src/assets 目录下所有符合规则的图片
|
||||
// 参数1:图片路径(@代表src目录,*为通配符)
|
||||
// 参数2:{ eager: true } 表示立即加载所有匹配的图片
|
||||
const imageModules = import.meta.glob('../../assets/members*.png', { eager: true })
|
||||
|
||||
// 转换为 { 图片ID: 图片路径 } 的映射表
|
||||
const imageMap: Record<number, string> = {}
|
||||
for (const path in imageModules) {
|
||||
// 从路径中提取图片ID(假设图片命名为 project1.png、project2.png...)
|
||||
const match = path.match(/members(\d+)\.png/)
|
||||
if (match) {
|
||||
const id = Number(match[1])
|
||||
// 存储图片路径(Vite 导入的图片默认在 default 属性中)
|
||||
imageMap[id] = (imageModules[path] as { default: string }).default
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟团队成员数据
|
||||
const teamMembers = ref<TeamMember[]>([
|
||||
{
|
||||
id: 1,
|
||||
nameKey: "team.member1.name",
|
||||
roleKey: "team.member1.role",
|
||||
bioKey: "team.member1.bio",
|
||||
photo: 'https://picsum.photos/id/1/300/300',
|
||||
//photo: imageMap[1],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nameKey: "team.member2.name",
|
||||
roleKey: "team.member2.role",
|
||||
bioKey: "team.member2.bio",
|
||||
photo: 'https://picsum.photos/id/2/300/300',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nameKey: "team.member3.name",
|
||||
roleKey: "team.member3.role",
|
||||
bioKey: "team.member3.bio",
|
||||
photo: 'https://picsum.photos/id/3/300/300',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nameKey: "team.member4.name",
|
||||
roleKey: "team.member4.role",
|
||||
bioKey: "team.member4.bio",
|
||||
photo: 'https://picsum.photos/id/4/300/300',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nameKey: "team.member5.name",
|
||||
roleKey: "team.member5.role",
|
||||
bioKey: "team.member5.bio",
|
||||
photo: 'https://picsum.photos/id/5/300/300',
|
||||
},
|
||||
])
|
||||
|
||||
// const store = useLanguageStore()
|
||||
// 注入翻译函数
|
||||
const t = inject<(key: string) => string>('t') || ((key) => key)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-section {
|
||||
padding: 5rem 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.team-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.team-card {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.team-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.team-photo {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.team-photo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.team-card:hover .team-photo img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.team-info {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.team-info h3 {
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.role {
|
||||
color: #4cc9f0;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.bio {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
text-align: left; /* 添加这一行,设置文本左对齐 */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
<template>
|
||||
<section id="team" class="team-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">{{ t('team.title') }}</h2>
|
||||
<div class="team-grid">
|
||||
<div v-for="member in teamMembers" :key="member.id" class="team-card">
|
||||
<div class="team-photo">
|
||||
<img :src="member.photo" :alt="t(member.nameKey)" />
|
||||
</div>
|
||||
<div class="team-info">
|
||||
<h3>{{ t(member.nameKey) }}</h3>
|
||||
<p class="role">{{ t('team.memberRole') }}: {{ t(member.roleKey) }}</p>
|
||||
<p class="bio">{{ t('team.memberBio') }}: {{ t(member.bioKey) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
|
||||
export interface TeamMember {
|
||||
id: number
|
||||
nameKey: string
|
||||
roleKey: string
|
||||
bioKey: string
|
||||
photo: string
|
||||
}
|
||||
|
||||
// 批量导入 src/assets 目录下所有符合规则的图片
|
||||
// 参数1:图片路径(@代表src目录,*为通配符)
|
||||
// 参数2:{ eager: true } 表示立即加载所有匹配的图片
|
||||
const imageModules = import.meta.glob('../../assets/members*.png', { eager: true })
|
||||
|
||||
// 转换为 { 图片ID: 图片路径 } 的映射表
|
||||
const imageMap: Record<number, string> = {}
|
||||
for (const path in imageModules) {
|
||||
// 从路径中提取图片ID(假设图片命名为 project1.png、project2.png...)
|
||||
const match = path.match(/members(\d+)\.png/)
|
||||
if (match) {
|
||||
const id = Number(match[1])
|
||||
// 存储图片路径(Vite 导入的图片默认在 default 属性中)
|
||||
imageMap[id] = (imageModules[path] as { default: string }).default
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟团队成员数据
|
||||
const teamMembers = ref<TeamMember[]>([
|
||||
{
|
||||
id: 1,
|
||||
nameKey: "team.member1.name",
|
||||
roleKey: "team.member1.role",
|
||||
bioKey: "team.member1.bio",
|
||||
photo: 'https://picsum.photos/id/1/300/300',
|
||||
//photo: imageMap[1],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nameKey: "team.member2.name",
|
||||
roleKey: "team.member2.role",
|
||||
bioKey: "team.member2.bio",
|
||||
photo: 'https://picsum.photos/id/2/300/300',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nameKey: "team.member3.name",
|
||||
roleKey: "team.member3.role",
|
||||
bioKey: "team.member3.bio",
|
||||
photo: 'https://picsum.photos/id/3/300/300',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nameKey: "team.member4.name",
|
||||
roleKey: "team.member4.role",
|
||||
bioKey: "team.member4.bio",
|
||||
photo: 'https://picsum.photos/id/4/300/300',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nameKey: "team.member5.name",
|
||||
roleKey: "team.member5.role",
|
||||
bioKey: "team.member5.bio",
|
||||
photo: 'https://picsum.photos/id/5/300/300',
|
||||
},
|
||||
])
|
||||
|
||||
// const store = useLanguageStore()
|
||||
// 注入翻译函数
|
||||
const t = inject<(key: string) => string>('t') || ((key) => key)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-section {
|
||||
padding: 5rem 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.team-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.team-card {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.team-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.team-photo {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.team-photo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.team-card:hover .team-photo img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.team-info {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.team-info h3 {
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.role {
|
||||
color: #4cc9f0;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.bio {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
text-align: left; /* 添加这一行,设置文本左对齐 */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
40
src/i18n.ts
40
src/i18n.ts
|
@ -1,21 +1,21 @@
|
|||
import { createI18n } from 'vue-i18n'
|
||||
import zh from './locales/zh'
|
||||
import en from './locales/en'
|
||||
import ja from './locales/ja'
|
||||
|
||||
// 定义语言 messages
|
||||
const messages = {
|
||||
zh,
|
||||
en,
|
||||
ja
|
||||
}
|
||||
|
||||
// 创建 i18n 实例
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API,必须设置为 false
|
||||
locale: 'ja', // 默认语言
|
||||
fallbackLocale: 'zh', // 回退语言
|
||||
messages
|
||||
})
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zh from './locales/zh'
|
||||
import en from './locales/en'
|
||||
import ja from './locales/ja'
|
||||
|
||||
// 定义语言 messages
|
||||
const messages = {
|
||||
zh,
|
||||
en,
|
||||
ja
|
||||
}
|
||||
|
||||
// 创建 i18n 实例
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 使用 Composition API,必须设置为 false
|
||||
locale: 'zh', // 默认语言
|
||||
fallbackLocale: 'ja', // 回退语言
|
||||
messages
|
||||
})
|
||||
|
||||
export default i18n
|
|
@ -1,51 +1,44 @@
|
|||
import enUs from 'tdesign-vue-next/es/locale/en_US';
|
||||
export default {
|
||||
nav: {
|
||||
home: 'Home',
|
||||
about: 'About Us',
|
||||
team: 'Our Team',
|
||||
projects: 'Projects',
|
||||
contact: 'Contact Us',
|
||||
},
|
||||
about: {
|
||||
title: 'About Us',
|
||||
mission: 'Our Mission',
|
||||
vision: 'Our Vision',
|
||||
advantages: 'Our Advantages',
|
||||
content: 'We are a professional software development company dedicated to providing high-quality solutions for our clients.',
|
||||
missionContent: 'To deliver innovative technology solutions that drive business growth and digital transformation.',
|
||||
visionContent: 'To become a leading global provider of software development services, recognized for our technical excellence and customer satisfaction.',
|
||||
advantage1: 'Technical Excellence',
|
||||
advantage2: 'Customer-Centric Approach',
|
||||
advantage3: 'Innovation',
|
||||
advantage4: 'Reliability',
|
||||
},
|
||||
team: {
|
||||
title: 'Our Team',
|
||||
memberRole: 'Role',
|
||||
memberBio: 'Bio',
|
||||
},
|
||||
projects: {
|
||||
title: 'Our Projects',
|
||||
viewDetails: 'View Details',
|
||||
},
|
||||
contact: {
|
||||
title: 'Contact Us',
|
||||
name: 'Your Name',
|
||||
email: 'Your Email',
|
||||
subject: 'Subject',
|
||||
message: 'Your Message',
|
||||
send: 'Send Message',
|
||||
success: 'Message sent successfully!',
|
||||
error: 'Failed to send message. Please try again.',
|
||||
},
|
||||
timeline: {
|
||||
title: 'Company History',
|
||||
},
|
||||
ai:{
|
||||
title: 'AIチャット',
|
||||
initContent: 'こんにちは、何かお手伝いできることはありますか?',
|
||||
clearHistory: '履歴をクリア',
|
||||
},
|
||||
...enUs.chat
|
||||
export default {
|
||||
nav: {
|
||||
home: 'Home',
|
||||
about: 'About Us',
|
||||
team: 'Our Team',
|
||||
projects: 'Projects',
|
||||
contact: 'Contact Us',
|
||||
},
|
||||
about: {
|
||||
title: 'About Us',
|
||||
mission: 'Our Mission',
|
||||
vision: 'Our Vision',
|
||||
advantages: 'Our Advantages',
|
||||
content: 'We are a professional software development company dedicated to providing high-quality solutions for our clients.',
|
||||
missionContent: 'To deliver innovative technology solutions that drive business growth and digital transformation.',
|
||||
visionContent: 'To become a leading global provider of software development services, recognized for our technical excellence and customer satisfaction.',
|
||||
advantage1: 'Technical Excellence',
|
||||
advantage2: 'Customer-Centric Approach',
|
||||
advantage3: 'Innovation',
|
||||
advantage4: 'Reliability',
|
||||
},
|
||||
team: {
|
||||
title: 'Our Team',
|
||||
memberRole: 'Role',
|
||||
memberBio: 'Bio',
|
||||
},
|
||||
projects: {
|
||||
title: 'Our Projects',
|
||||
viewDetails: 'View Details',
|
||||
},
|
||||
contact: {
|
||||
title: 'Contact Us',
|
||||
name: 'Your Name',
|
||||
email: 'Your Email',
|
||||
subject: 'Subject',
|
||||
message: 'Your Message',
|
||||
send: 'Send Message',
|
||||
success: 'Message sent successfully!',
|
||||
error: 'Failed to send message. Please try again.',
|
||||
},
|
||||
timeline: {
|
||||
title: 'Company History',
|
||||
},
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
// 导入所需的语言包
|
||||
import zhConfig from 'tdesign-vue-next/es/locale/zh_CN';
|
||||
import enConfig from 'tdesign-vue-next/es/locale/en_US';
|
||||
import jaConfig from 'tdesign-vue-next/es/locale/ja_JP';
|
||||
import { merge } from 'lodash-es';
|
||||
import { type GlobalConfigProvider } from 'tdesign-vue-next';
|
||||
import { ref, watch, computed } from 'vue';
|
||||
|
||||
|
||||
import { useLanguageStore } from '../store/language'
|
||||
|
||||
const store = useLanguageStore()
|
||||
|
||||
// 定义支持的语言类型
|
||||
// export type Language = 'zh-CN' | 'en-US' | 'ja-JP';
|
||||
export type Language = 'zh' | 'en' | 'ja';
|
||||
|
||||
// 明确类型为 Language
|
||||
const currentLanguage = computed<Language>(() => store.currentLanguage);
|
||||
|
||||
|
||||
|
||||
|
||||
// 基础自定义配置
|
||||
const customConfig: GlobalConfigProvider = {
|
||||
calendar: {},
|
||||
table: {},
|
||||
pagination: {},
|
||||
chat:{},
|
||||
// 可以添加更多自定义配置
|
||||
};
|
||||
|
||||
// 语言包映射(使用 any 类型来绕过严格的类型检查)
|
||||
const localeMap: Record<Language, any> = {
|
||||
'zh': zhConfig,
|
||||
'en': enConfig,
|
||||
'ja': jaConfig,
|
||||
};
|
||||
|
||||
// 响应式全局配置
|
||||
const globalConfig = ref<GlobalConfigProvider>(
|
||||
merge({}, localeMap[currentLanguage.value] || localeMap['zh'], customConfig) as GlobalConfigProvider
|
||||
);
|
||||
|
||||
// 当语言变化时更新全局配置
|
||||
watch(currentLanguage, (newLang) => {
|
||||
// 更新全局配置
|
||||
const lang = newLang as unknown as Language;
|
||||
globalConfig.value = merge({}, localeMap[lang] || localeMap['zh'], customConfig) as GlobalConfigProvider;
|
||||
console.log(globalConfig.value)
|
||||
});
|
||||
|
||||
|
||||
export {
|
||||
currentLanguage,
|
||||
globalConfig
|
||||
};
|
|
@ -1,5 +1,3 @@
|
|||
import jaJP from 'tdesign-vue-next/es/locale/ja_JP';
|
||||
|
||||
export default {
|
||||
nav: {
|
||||
name:'優閲スタジオ',
|
||||
|
@ -119,13 +117,6 @@ export default {
|
|||
timeline: {
|
||||
title: 'タイムライン',
|
||||
},
|
||||
ai:{
|
||||
title: 'AIチャット',
|
||||
initContent: 'こんにちは、何かお手伝いできることはありますか?',
|
||||
clearHistory: '履歴をクリア',
|
||||
},
|
||||
|
||||
|
||||
chat: {
|
||||
...jaJP.chat
|
||||
}
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
import znCh from 'tdesign-vue-next/es/locale/zh_CN';
|
||||
|
||||
export default {
|
||||
nav: {
|
||||
name:'优阅工作室',
|
||||
|
@ -116,13 +113,4 @@ export default {
|
|||
timeline: {
|
||||
title: '项目经历',
|
||||
},
|
||||
ai:{
|
||||
title: 'AI助手',
|
||||
initContent: '我是你的ai助手,有什么可以帮你的。',
|
||||
clearHistory: "清空历史记录11",
|
||||
|
||||
},
|
||||
chat: {
|
||||
...znCh.chat
|
||||
}
|
||||
}
|
60
src/main.ts
60
src/main.ts
|
@ -1,33 +1,27 @@
|
|||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router/index.ts' // 确保路径正确
|
||||
|
||||
// 引入Pinia
|
||||
import { createPinia } from 'pinia'
|
||||
// 引入TDesign UI组件库
|
||||
import TDesign from 'tdesign-vue-next'
|
||||
import TDesignChat from '@tdesign-vue-next/chat'; // 引入chat组件
|
||||
import 'tdesign-vue-next/es/style/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
// 必须先 use(router),再挂载
|
||||
app.use(router)
|
||||
|
||||
// 使用Pinia
|
||||
app.use(createPinia())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 使用TDesign
|
||||
app.use(TDesign)
|
||||
app.use(TDesignChat)
|
||||
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
// 临时打印路由列表,确认配置是否生效
|
||||
console.log('已注册的路由:', router.getRoutes().map(r => r.path));
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router/index.ts' // 确保路径正确
|
||||
|
||||
|
||||
|
||||
// 引入Pinia
|
||||
import { createPinia } from 'pinia'
|
||||
// 引入TDesign UI组件库
|
||||
import TDesign from 'tdesign-vue-next'
|
||||
import 'tdesign-vue-next/es/style/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
// 必须先 use(router),再挂载
|
||||
app.use(router)
|
||||
|
||||
// 使用Pinia
|
||||
app.use(createPinia())
|
||||
|
||||
// 使用TDesign
|
||||
app.use(TDesign)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
// 临时打印路由列表,确认配置是否生效
|
||||
console.log('已注册的路由:', router.getRoutes().map(r => r.path));
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
// src/router/index.ts
|
||||
import { createRouter, createWebHistory} from 'vue-router';
|
||||
import Project3Jp from '@/views/ProjectDetail/Project3Jp.vue';
|
||||
import Project3Zh from '@/views/ProjectDetail/Project3Zh.vue';
|
||||
import Project4Jp from '@/views/ProjectDetail/Project4Jp.vue';
|
||||
import Project4Zh from '@/views/ProjectDetail/Project4Zh.vue';
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
// 首页路由
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/HomePage.vue'),
|
||||
meta: { hideHeader: false }, // 主页显示公共组件
|
||||
},
|
||||
// ✅ 项目详情页
|
||||
// 项目3 - 中文
|
||||
{
|
||||
path: '/project3/zh',
|
||||
name: 'project3Detail', // 中文路由名称
|
||||
component: Project3Zh,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 项目3 - 日文
|
||||
{
|
||||
path: '/project3/jp',
|
||||
name: 'project3DetailJp', // 日文路由名称
|
||||
component: Project3Jp,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 项目4 - 中文
|
||||
{
|
||||
path: '/project4/zh',
|
||||
name: 'project4Detail', // 中文路由名称
|
||||
component: Project4Zh,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 项目4 - 日文
|
||||
{
|
||||
path: '/project4/jp',
|
||||
name: 'project4DetailJp', // 日文路由名称
|
||||
component: Project4Jp,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 其他路由...
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
// src/router/index.ts
|
||||
import { createRouter, createWebHistory} from 'vue-router';
|
||||
import Project3Jp from '@/views/ProjectDetail/Project3Jp.vue';
|
||||
import Project3Zh from '@/views/ProjectDetail/Project3Zh.vue';
|
||||
import Project4Jp from '@/views/ProjectDetail/Project4Jp.vue';
|
||||
import Project4Zh from '@/views/ProjectDetail/Project4Zh.vue';
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
// 首页路由
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/HomePage.vue'),
|
||||
meta: { hideHeader: false }, // 主页显示公共组件
|
||||
},
|
||||
// ✅ 项目详情页
|
||||
// 项目3 - 中文
|
||||
{
|
||||
path: '/project3/zh',
|
||||
name: 'project3Detail', // 中文路由名称
|
||||
component: Project3Zh,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 项目3 - 日文
|
||||
{
|
||||
path: '/project3/jp',
|
||||
name: 'project3DetailJp', // 日文路由名称
|
||||
component: Project3Jp,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 项目4 - 中文
|
||||
{
|
||||
path: '/project4/zh',
|
||||
name: 'project4Detail', // 中文路由名称
|
||||
component: Project4Zh,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 项目4 - 日文
|
||||
{
|
||||
path: '/project4/jp',
|
||||
name: 'project4DetailJp', // 日文路由名称
|
||||
component: Project4Jp,
|
||||
meta: { hideHeader: true },
|
||||
|
||||
},
|
||||
// 其他路由...
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
|
@ -1,40 +1,40 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export class EmailService {
|
||||
static async sendEmail(data: {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
}): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
}> {
|
||||
try {
|
||||
// 在实际应用中,这里应该指向你的后端API
|
||||
// 为了演示,我们使用一个模拟的响应
|
||||
// const response = await axios.post('/api/email', data)
|
||||
|
||||
const response = await axios.post('/api/mail/send ', data)
|
||||
|
||||
console.log('Email sent:', response.data)
|
||||
// 模拟成功响应
|
||||
return {
|
||||
success: true,
|
||||
message: 'Email sent successfully',
|
||||
}
|
||||
|
||||
// 如果需要处理错误,可以取消上面的注释并使用下面的代码
|
||||
// return {
|
||||
// success: response.data.success,
|
||||
// message: response.data.message,
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error('Failed to send email:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to send email. Please try again later.',
|
||||
}
|
||||
}
|
||||
}
|
||||
import axios from 'axios'
|
||||
|
||||
export class EmailService {
|
||||
static async sendEmail(data: {
|
||||
name: string
|
||||
email: string
|
||||
subject: string
|
||||
message: string
|
||||
}): Promise<{
|
||||
success: boolean
|
||||
message: string
|
||||
}> {
|
||||
try {
|
||||
// 在实际应用中,这里应该指向你的后端API
|
||||
// 为了演示,我们使用一个模拟的响应
|
||||
// const response = await axios.post('/api/email', data)
|
||||
|
||||
const response = await axios.post('/api/mail/send ', data)
|
||||
|
||||
console.log('Email sent:', response.data)
|
||||
// 模拟成功响应
|
||||
return {
|
||||
success: true,
|
||||
message: 'Email sent successfully',
|
||||
}
|
||||
|
||||
// 如果需要处理错误,可以取消上面的注释并使用下面的代码
|
||||
// return {
|
||||
// success: response.data.success,
|
||||
// message: response.data.message,
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error('Failed to send email:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to send email. Please try again later.',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// src/shims-vue.d.ts
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
// src/shims-vue.d.ts
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
|
@ -1,49 +1,50 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import zh from '../locales/zh'
|
||||
import ja from '../locales/ja'
|
||||
|
||||
// 获取浏览器语言
|
||||
function getBrowserLanguage() {
|
||||
// 获取浏览器主语言(如从 "zh-CN" 中提取 "zh-CN" 或从 "zh-TW" 中提取 "zh-TW")
|
||||
const browserLang = navigator.language;
|
||||
if (browserLang.startsWith('zh-CN')) {
|
||||
return 'zh';
|
||||
}
|
||||
// 默认返回英语
|
||||
return 'ja';
|
||||
}
|
||||
const lang = getBrowserLanguage()
|
||||
export const useLanguageStore = defineStore('language', {
|
||||
state: () => ({
|
||||
currentLanguage: lang, // 默认中文
|
||||
messages: {
|
||||
zh,
|
||||
ja,
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
t: (state) => (key: string) => {
|
||||
// 简单的键路径解析,如 'nav.home'
|
||||
const keys = key.split('.')
|
||||
let value = state.messages[state.currentLanguage]
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k]
|
||||
} else {
|
||||
return key // 如果找不到翻译,返回原始键
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleLanguage() {
|
||||
this.currentLanguage = this.currentLanguage === 'zh' ? 'ja' : 'zh'
|
||||
},
|
||||
setLanguage(lang: 'zh' | 'ja') {
|
||||
this.currentLanguage = lang
|
||||
},
|
||||
},
|
||||
import { defineStore } from 'pinia'
|
||||
import zh from '../locales/zh'
|
||||
import ja from '../locales/ja'
|
||||
|
||||
|
||||
// 获取浏览器语言
|
||||
function getBrowserLanguage() {
|
||||
// 获取浏览器主语言(如从 "zh-CN" 中提取 "zh-CN" 或从 "zh-TW" 中提取 "zh-TW")
|
||||
const browserLang = navigator.language;
|
||||
if (browserLang.startsWith('zh-CN')) {
|
||||
return 'zh';
|
||||
}
|
||||
// 默认返回英语
|
||||
return 'en';
|
||||
}
|
||||
const lang = getBrowserLanguage()
|
||||
export const useLanguageStore = defineStore('language', {
|
||||
state: () => ({
|
||||
currentLanguage: lang, // 默认中文
|
||||
messages: {
|
||||
zh,
|
||||
ja,
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
t: (state) => (key: string) => {
|
||||
// 简单的键路径解析,如 'nav.home'
|
||||
const keys = key.split('.')
|
||||
let value = state.messages[state.currentLanguage]
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k]
|
||||
} else {
|
||||
return key // 如果找不到翻译,返回原始键
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleLanguage() {
|
||||
this.currentLanguage = this.currentLanguage === 'zh' ? 'ja' : 'zh'
|
||||
},
|
||||
setLanguage(lang: 'zh' | 'ja') {
|
||||
this.currentLanguage = lang
|
||||
},
|
||||
},
|
||||
})
|
356
src/style.css
356
src/style.css
|
@ -1,178 +1,178 @@
|
|||
:root {
|
||||
font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background-color: #121212;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
--primary-color: #3b82f6;
|
||||
--primary-light: #60a5fa;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary-color: #10b981;
|
||||
--text-color: #f3f4f6;
|
||||
--text-muted: #9ca3af;
|
||||
--bg-color: #121212;
|
||||
--bg-card: #1e1e1e;
|
||||
--border-color: #374151;
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--primary-light);
|
||||
text-decoration: inherit;
|
||||
transition: var(--transition);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
button:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--primary-light);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
background-color: var(--bg-card);
|
||||
box-shadow: var(--shadow);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 5rem 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 2.5rem;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.section-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #1f2937;
|
||||
background-color: #ffffff;
|
||||
--primary-color: #3b82f6;
|
||||
--primary-light: #60a5fa;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary-color: #10b981;
|
||||
--text-color: #1f2937;
|
||||
--text-muted: #6b7280;
|
||||
--bg-color: #ffffff;
|
||||
--bg-card: #f9fafb;
|
||||
--border-color: #e5e7eb;
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background-color: #121212;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
--primary-color: #3b82f6;
|
||||
--primary-light: #60a5fa;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary-color: #10b981;
|
||||
--text-color: #f3f4f6;
|
||||
--text-muted: #9ca3af;
|
||||
--bg-color: #121212;
|
||||
--bg-card: #1e1e1e;
|
||||
--border-color: #374151;
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--primary-light);
|
||||
text-decoration: inherit;
|
||||
transition: var(--transition);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
button:hover {
|
||||
background-color: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--primary-light);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
background-color: var(--bg-card);
|
||||
box-shadow: var(--shadow);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 5rem 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 2.5rem;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.section-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #1f2937;
|
||||
background-color: #ffffff;
|
||||
--primary-color: #3b82f6;
|
||||
--primary-light: #60a5fa;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary-color: #10b981;
|
||||
--text-color: #1f2937;
|
||||
--text-muted: #6b7280;
|
||||
--bg-color: #ffffff;
|
||||
--bg-card: #f9fafb;
|
||||
--border-color: #e5e7eb;
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,6 @@
|
|||
|
||||
<!-- 联系我们 -->
|
||||
<ContactForm />
|
||||
|
||||
<ChatAi />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -39,8 +37,6 @@ import TeamMembers from '@/components/team/TeamMembers.vue'
|
|||
import ProjectShowcase from '@/components/projects/ProjectShowcase.vue'
|
||||
import ContactForm from '@/components/contact/ContactForm.vue'
|
||||
import CompanyTimeline from '@/components/timeline/CompanyTimeline.vue'
|
||||
|
||||
import ChatAi from '@/components/chat/ChatAi.vue'
|
||||
import { inject } from 'vue'
|
||||
|
||||
// 注入翻译函数(与原App.vue保持一致)
|
||||
|
@ -49,6 +45,9 @@ const t = inject<(key: string) => string>('t') || ((key) => key)
|
|||
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
|
||||
.hero-section {
|
||||
height: 100vh;
|
||||
background-color: var(--bg-color);
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
<!-- src/views/ProjectDetail/Project3Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 勤怠登録画面 -->
|
||||
<h2>勤怠登録画面</h2>
|
||||
<img :src="img1" alt="勤怠登录页面">
|
||||
|
||||
<!-- 休暇申請画面 -->
|
||||
<h2>休暇申請画面</h2>
|
||||
<img :src="img2" alt="休假申请页面">
|
||||
|
||||
<!-- 勤怠集計画面 -->
|
||||
<h2>勤怠集計画面</h2>
|
||||
<img :src="img3" alt="勤怠集计页面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project3/project3-1.png'
|
||||
import img2 from '@/assets/project3/project3-2.png'
|
||||
import img3 from '@/assets/project3/project3-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project3Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- src/views/ProjectDetail/Project3Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 勤怠登録画面 -->
|
||||
<h2>勤怠登録画面</h2>
|
||||
<img :src="img1" alt="勤怠登录页面">
|
||||
|
||||
<!-- 休暇申請画面 -->
|
||||
<h2>休暇申請画面</h2>
|
||||
<img :src="img2" alt="休假申请页面">
|
||||
|
||||
<!-- 勤怠集計画面 -->
|
||||
<h2>勤怠集計画面</h2>
|
||||
<img :src="img3" alt="勤怠集计页面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project3/project3-1.png'
|
||||
import img2 from '@/assets/project3/project3-2.png'
|
||||
import img3 from '@/assets/project3/project3-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project3Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
<!-- src/views/ProjectDetail/Project4Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 勤怠登录页面 -->
|
||||
<h2>勤怠登录页面</h2>
|
||||
<img :src="img1" alt="勤怠登录页面">
|
||||
|
||||
<!-- 休假申请页面 -->
|
||||
<h2>休假申请页面</h2>
|
||||
<img :src="img2" alt="休假申请页面">
|
||||
|
||||
<!-- 勤怠集计页面 -->
|
||||
<h2>勤怠集计页面</h2>
|
||||
<img :src="img3" alt="勤怠集计页面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project3/project3-1.png'
|
||||
import img2 from '@/assets/project3/project3-2.png'
|
||||
import img3 from '@/assets/project3/project3-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project3Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- src/views/ProjectDetail/Project4Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 勤怠登录页面 -->
|
||||
<h2>勤怠登录页面</h2>
|
||||
<img :src="img1" alt="勤怠登录页面">
|
||||
|
||||
<!-- 休假申请页面 -->
|
||||
<h2>休假申请页面</h2>
|
||||
<img :src="img2" alt="休假申请页面">
|
||||
|
||||
<!-- 勤怠集计页面 -->
|
||||
<h2>勤怠集计页面</h2>
|
||||
<img :src="img3" alt="勤怠集计页面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project3/project3-1.png'
|
||||
import img2 from '@/assets/project3/project3-2.png'
|
||||
import img3 from '@/assets/project3/project3-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project3Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
<!-- src/views/ProjectDetail/Project4Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 資材管理画面 -->
|
||||
<h2>資材管理画面</h2>
|
||||
<img :src="img1" alt="資材管理画面">
|
||||
|
||||
<!-- 工程管理画面 -->
|
||||
<h2>工程管理画面</h2>
|
||||
<img :src="img2" alt="工程管理画面">
|
||||
|
||||
<!-- 設備保全画面 -->
|
||||
<h2>設備保全画面</h2>
|
||||
<img :src="img3" alt="設備保全画面">
|
||||
|
||||
<!-- 生産管理画面 -->
|
||||
<h2>生産管理画面</h2>
|
||||
<img :src="img4" alt="生産管理画面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project4/project4-1.png'
|
||||
import img2 from '@/assets/project4/project4-2.png'
|
||||
import img3 from '@/assets/project4/project4-3.png'
|
||||
import img4 from '@/assets/project4/project4-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project3Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- src/views/ProjectDetail/Project4Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 資材管理画面 -->
|
||||
<h2>資材管理画面</h2>
|
||||
<img :src="img1" alt="資材管理画面">
|
||||
|
||||
<!-- 工程管理画面 -->
|
||||
<h2>工程管理画面</h2>
|
||||
<img :src="img2" alt="工程管理画面">
|
||||
|
||||
<!-- 設備保全画面 -->
|
||||
<h2>設備保全画面</h2>
|
||||
<img :src="img3" alt="設備保全画面">
|
||||
|
||||
<!-- 生産管理画面 -->
|
||||
<h2>生産管理画面</h2>
|
||||
<img :src="img4" alt="生産管理画面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project4/project4-1.png'
|
||||
import img2 from '@/assets/project4/project4-2.png'
|
||||
import img3 from '@/assets/project4/project4-3.png'
|
||||
import img4 from '@/assets/project4/project4-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project3Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
<!-- src/views/ProjectDetail/Project4Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 物料管理页面 -->
|
||||
<h2>物料管理页面</h2>
|
||||
<img :src="img1" alt="物料管理页面">
|
||||
|
||||
<!-- 工艺管理页面 -->
|
||||
<h2>工艺管理页面</h2>
|
||||
<img :src="img2" alt="工艺管理页面">
|
||||
|
||||
<!-- 设备保养页面 -->
|
||||
<h2>设备保养页面</h2>
|
||||
<img :src="img3" alt="设备保养页面">
|
||||
|
||||
<!-- 生产管理页面 -->
|
||||
<h2>生产管理页面</h2>
|
||||
<img :src="img4" alt="生产管理页面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project4/project4-1.png'
|
||||
import img2 from '@/assets/project4/project4-2.png'
|
||||
import img3 from '@/assets/project4/project4-3.png'
|
||||
import img4 from '@/assets/project4/project4-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project4Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- src/views/ProjectDetail/Project4Zh.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 物料管理页面 -->
|
||||
<h2>物料管理页面</h2>
|
||||
<img :src="img1" alt="物料管理页面">
|
||||
|
||||
<!-- 工艺管理页面 -->
|
||||
<h2>工艺管理页面</h2>
|
||||
<img :src="img2" alt="工艺管理页面">
|
||||
|
||||
<!-- 设备保养页面 -->
|
||||
<h2>设备保养页面</h2>
|
||||
<img :src="img3" alt="设备保养页面">
|
||||
|
||||
<!-- 生产管理页面 -->
|
||||
<h2>生产管理页面</h2>
|
||||
<img :src="img4" alt="生产管理页面">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img1 from '@/assets/project4/project4-1.png'
|
||||
import img2 from '@/assets/project4/project4-2.png'
|
||||
import img3 from '@/assets/project4/project4-3.png'
|
||||
import img4 from '@/assets/project4/project4-3.png'
|
||||
|
||||
// 可选:定义组件名(仅用于调试)
|
||||
defineOptions({
|
||||
name: 'Project4Zh'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
// 为了在模板中使用 $t 函数,我们需要扩展 Vue 的类型定义
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$t: (key: string) => string
|
||||
}
|
||||
}
|
||||
|
||||
// 为 TDesign 组件添加类型支持
|
||||
declare module 'tdesign-vue-next' {
|
||||
export * from 'tdesign-vue-next/es'
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// 为了在模板中使用 $t 函数,我们需要扩展 Vue 的类型定义
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$t: (key: string) => string
|
||||
}
|
||||
}
|
||||
|
||||
// 为 TDesign 组件添加类型支持
|
||||
declare module 'tdesign-vue-next' {
|
||||
export * from 'tdesign-vue-next/es'
|
||||
}
|
|
@ -1,23 +1,23 @@
|
|||
// src/vue-shims.d.ts
|
||||
// Vue 3核心API类型声明
|
||||
|
||||
declare module 'vue' {
|
||||
export * from '@vue/runtime-dom';
|
||||
export { default } from '@vue/runtime-dom';
|
||||
}
|
||||
|
||||
// 为Composition API添加类型声明
|
||||
declare module '@vue/runtime-dom' {
|
||||
export function ref<T>(value: T): import('@vue/reactivity').Ref<T>;
|
||||
export function inject<T>(key: string | Symbol, defaultValue?: T): T | undefined;
|
||||
export function provide<T>(key: string | Symbol, value: T): void;
|
||||
export function createApp(rootComponent: import('@vue/runtime-core').Component): import('@vue/runtime-core').App;
|
||||
export function defineProps<Props>(): Props;
|
||||
export function defineEmits<Emits extends Record<string, (...args: any[]) => any>>(emits?: Emits): Emits;
|
||||
export function computed<T>(getter: () => T): import('@vue/reactivity').ComputedRef<T>;
|
||||
export function watch<T>(source: T | (() => T), callback: (newValue: T, oldValue: T) => void): void;
|
||||
export function onMounted(callback: () => void): void;
|
||||
export function onUnmounted(callback: () => void): void;
|
||||
export function onUpdated(callback: () => void): void;
|
||||
export function onBeforeUpdate(callback: () => void): void;
|
||||
// src/vue-shims.d.ts
|
||||
// Vue 3核心API类型声明
|
||||
|
||||
declare module 'vue' {
|
||||
export * from '@vue/runtime-dom';
|
||||
export { default } from '@vue/runtime-dom';
|
||||
}
|
||||
|
||||
// 为Composition API添加类型声明
|
||||
declare module '@vue/runtime-dom' {
|
||||
export function ref<T>(value: T): import('@vue/reactivity').Ref<T>;
|
||||
export function inject<T>(key: string | Symbol, defaultValue?: T): T | undefined;
|
||||
export function provide<T>(key: string | Symbol, value: T): void;
|
||||
export function createApp(rootComponent: import('@vue/runtime-core').Component): import('@vue/runtime-core').App;
|
||||
export function defineProps<Props>(): Props;
|
||||
export function defineEmits<Emits extends Record<string, (...args: any[]) => any>>(emits?: Emits): Emits;
|
||||
export function computed<T>(getter: () => T): import('@vue/reactivity').ComputedRef<T>;
|
||||
export function watch<T>(source: T | (() => T), callback: (newValue: T, oldValue: T) => void): void;
|
||||
export function onMounted(callback: () => void): void;
|
||||
export function onUnmounted(callback: () => void): void;
|
||||
export function onUpdated(callback: () => void): void;
|
||||
export function onBeforeUpdate(callback: () => void): void;
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/stores/layout"]
|
||||
}
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/stores/layout"]
|
||||
}
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'), // 将 @ 指向 src 目录
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // 监听所有网络接口
|
||||
port: 5173, // 可自定义端口(默认5173)
|
||||
open: false // 可选:是否自动打开浏览器
|
||||
}
|
||||
})
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'), // 将 @ 指向 src 目录
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue