first commit

This commit is contained in:
aaron 2025-04-25 11:09:29 +08:00
commit 23dc8df4a9
41 changed files with 3130 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
test-results/
playwright-report/

6
.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

10
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"ms-playwright.playwright",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# .
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```
### Run End-to-End Tests with [Playwright](https://playwright.dev)
```sh
# Install browsers for the first run
npx playwright install
# When testing on CI, must build the project first
npm run build
# Runs the end-to-end tests
npm run test:e2e
# Runs the tests only on Chromium
npm run test:e2e -- --project=chromium
# Runs the tests of a specific file
npm run test:e2e -- tests/example.spec.ts
# Runs the tests in debug mode
npm run test:e2e -- --debug
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

4
e2e/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": ["./**/*"]
}

8
e2e/vue.spec.ts Normal file
View File

@ -0,0 +1,8 @@
import { test, expect } from '@playwright/test';
// See here how to get started:
// https://playwright.dev/docs/intro
test('visits the app root url', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toHaveText('You did it!');
})

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

34
eslint.config.ts Normal file
View File

@ -0,0 +1,34 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginVitest from '@vitest/eslint-plugin'
import pluginPlaywright from 'eslint-plugin-playwright'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
{
...pluginVitest.configs.recommended,
files: ['src/**/__tests__/*'],
},
{
...pluginPlaywright.configs['flat/recommended'],
files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'],
},
skipFormatting,
)

30
index.html Normal file
View File

@ -0,0 +1,30 @@
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Crypto AI - 加密货币工具与社区平台</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
overflow-x: hidden;
}
body {
background-color: #0f1318;
color: #ffffff;
}
#app {
width: 100%;
overflow-x: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "icrypto",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"test:e2e": "playwright test",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix",
"format": "prettier --write src/"
},
"dependencies": {
"pinia": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@playwright/test": "^1.51.1",
"@tsconfig/node22": "^22.0.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.14.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vitest/eslint-plugin": "^1.1.39",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.22.0",
"eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-vue": "~10.0.0",
"jiti": "^2.4.2",
"jsdom": "^26.0.0",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.3",
"typescript": "~5.8.0",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2",
"vitest": "^3.1.1",
"vue-tsc": "^2.2.8"
}
}

110
playwright.config.ts Normal file
View File

@ -0,0 +1,110 @@
import process from 'node:process'
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Only on CI systems run the tests headless */
headless: !!process.env.CI,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
webServer: {
/**
* Use the dev server by default for faster feedback loop.
* Use the preview server on CI for more realistic testing.
* Playwright will re-use the local server if there is already a dev-server running.
*/
command: process.env.CI ? 'npm run preview' : 'npm run dev',
port: process.env.CI ? 4173 : 5173,
reuseExistingServer: !process.env.CI,
},
})

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

342
src/App.vue Normal file
View File

@ -0,0 +1,342 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<div class="app-container">
<header class="app-header">
<div class="header-content">
<div class="logo">Crypto.AI</div>
<nav class="main-nav">
<RouterLink to="/" class="nav-link">首页</RouterLink>
<RouterLink to="/tools" class="nav-link">工具集合</RouterLink>
<RouterLink to="/ai-agent" class="nav-link">AI Agent</RouterLink>
<RouterLink to="/community" class="nav-link">社区</RouterLink>
</nav>
<div class="user-actions">
<button class="btn-connect">连接钱包</button>
</div>
</div>
</header>
<main class="app-content">
<div class="content-container">
<RouterView />
</div>
</main>
<footer class="app-footer">
<div class="footer-content">
<p>&copy; 2024 Crypto.AI - 加密货币工具与社区平台</p>
</div>
</footer>
</div>
</template>
<style>
:root {
--color-bg-primary: #000000;
--color-bg-secondary: #111111;
--color-bg-elevated: #1a1a1a;
--color-bg-card: #0a0a0a;
--color-text-primary: rgba(255, 255, 255, 1);
--color-text-secondary: rgba(255, 255, 255, 0.7);
--color-text-tertiary: rgba(255, 255, 255, 0.5);
--color-text-disabled: rgba(255, 255, 255, 0.3);
--color-divider: rgba(255, 255, 255, 0.1);
--color-border: rgba(255, 255, 255, 0.15);
--color-border-hover: rgba(255, 255, 255, 0.25);
--color-accent: rgba(255, 255, 255, 0.9);
--color-accent-hover: rgba(255, 255, 255, 1);
--color-accent-light: rgba(255, 255, 255, 0.1);
--font-weight-light: 300;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
--header-height: 70px;
--border-radius: 8px;
--max-content-width: 1280px;
--content-padding: 1rem;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#app {
width: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
body {
font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
line-height: 1.6;
min-height: 100vh;
box-sizing: border-box;
font-weight: var(--font-weight-regular);
}
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
width: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
box-sizing: border-box;
position: relative;
}
.app-header {
background-color: var(--color-bg-secondary);
box-shadow: 0 1px 0 var(--color-divider);
height: var(--header-height);
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(8px);
width: 100vw;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
left: 0;
right: 0;
position: relative;
}
.header-content {
width: 100%;
max-width: var(--max-content-width);
margin: 0 auto;
padding: 0 var(--content-padding);
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
.logo {
font-size: 1.8rem;
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: 0.5px;
}
.main-nav {
display: flex;
gap: 2rem;
height: 100%;
}
.nav-link {
color: var(--color-text-secondary);
text-decoration: none;
font-weight: var(--font-weight-medium);
transition: all 0.2s ease;
padding: 0 0.5rem;
height: 100%;
display: flex;
align-items: center;
position: relative;
}
.nav-link:hover {
color: var(--color-text-primary);
}
.router-link-active {
color: var(--color-text-primary);
font-weight: var(--font-weight-bold);
}
.router-link-active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: var(--color-accent);
}
.user-actions {
display: flex;
align-items: center;
}
.btn-connect {
background-color: var(--color-bg-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
padding: 0.7rem 1.4rem;
border-radius: var(--border-radius);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all 0.2s ease;
}
.btn-connect:hover {
background-color: var(--color-bg-secondary);
border-color: var(--color-border-hover);
}
.app-content {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 0;
}
.content-container {
width: 100%;
max-width: var(--max-content-width);
padding: 0 var(--content-padding);
display: flex;
flex-direction: column;
}
.app-footer {
background-color: var(--color-bg-secondary);
border-top: 1px solid var(--color-divider);
font-size: 0.9rem;
width: 100vw;
margin: 0;
padding: 1.5rem 0;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
left: 0;
right: 0;
position: relative;
}
.footer-content {
width: 100%;
max-width: var(--max-content-width);
margin: 0 auto;
padding: 0 var(--content-padding);
text-align: center;
color: var(--color-text-tertiary);
}
/* 全局按钮样式 */
.btn {
padding: 0.8rem 1.6rem;
border-radius: var(--border-radius);
font-weight: var(--font-weight-medium);
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-primary {
background-color: var(--color-bg-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-primary:hover {
background-color: var(--color-bg-secondary);
border-color: var(--color-border-hover);
}
.btn-secondary {
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background-color: var(--color-bg-elevated);
color: var(--color-text-primary);
}
/* 全局卡片样式 */
.card {
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
padding: 1.5rem;
border: 1px solid var(--color-border);
transition: all 0.3s ease;
}
.card:hover {
border-color: var(--color-border-hover);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
/* 响应式设计 */
@media (max-width: 1280px) {
:root {
--content-padding: 2rem;
}
.content-container,
.header-content,
.footer-content {
width: 100%;
max-width: var(--max-content-width);
}
}
@media (max-width: 768px) {
:root {
--content-padding: 1rem;
}
.header-content {
flex-direction: column;
height: auto;
padding: 1rem var(--content-padding);
}
.app-header {
height: auto;
position: relative;
}
.main-nav {
width: 100%;
justify-content: space-between;
gap: 0.5rem;
margin: 1rem 0;
height: auto;
}
.nav-link {
padding: 0.75rem 0;
height: auto;
}
.user-actions {
width: 100%;
justify-content: center;
margin-bottom: 0.5rem;
}
.app-content {
padding: 1rem 0;
}
}
</style>

98
src/assets/base.css Normal file
View File

@ -0,0 +1,98 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: rgba(255, 255, 255, 0.8);
--vt-c-white-mute: rgba(255, 255, 255, 0.5);
--vt-c-black: #000000;
--vt-c-black-soft: #0a0a0a;
--vt-c-black-mute: #111111;
--vt-c-divider-light-1: rgba(255, 255, 255, 0.15);
--vt-c-divider-light-2: rgba(255, 255, 255, 0.1);
--vt-c-divider-dark-1: rgba(255, 255, 255, 0.15);
--vt-c-divider-dark-2: rgba(255, 255, 255, 0.1);
--vt-c-text-light-1: var(--vt-c-white);
--vt-c-text-light-2: var(--vt-c-white-soft);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: var(--vt-c-white-soft);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
font-weight: normal;
}
html,
body {
width: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
body {
min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
flex-direction: column;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

37
src/assets/main.css Normal file
View File

@ -0,0 +1,37 @@
@import './base.css';
#app {
width: 100%;
margin: 0;
padding: 0;
font-weight: normal;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
a {
text-decoration: none;
color: rgba(255, 255, 255, 0.9);
transition: color 0.2s ease;
}
a:hover {
color: rgba(255, 255, 255, 1);
}
@media (min-width: 1024px) {
body {
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
}
#app {
display: flex;
flex-direction: column;
width: 100%;
padding: 0;
}
}

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

14
src/main.ts Normal file
View File

@ -0,0 +1,14 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

30
src/router/index.ts Normal file
View File

@ -0,0 +1,30 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/tools',
name: 'tools',
component: () => import('../views/ToolsView.vue'),
},
{
path: '/ai-agent',
name: 'ai-agent',
component: () => import('../views/AIAgentView.vue'),
},
{
path: '/community',
name: 'community',
component: () => import('../views/CommunityView.vue'),
},
],
})
export default router

12
src/stores/counter.ts Normal file
View File

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

410
src/views/AIAgentView.vue Normal file
View File

@ -0,0 +1,410 @@
<script setup lang="ts">
import { ref } from 'vue'
const activeAgent = ref('crypto-doctor')
const userInput = ref('')
const chatHistory = ref([
{
role: 'assistant',
content:
'你好!我是加密项目医生,可以帮你分析任何加密项目的健康状况。请告诉我你想了解的项目名称或合约地址。',
},
])
const sendMessage = () => {
if (!userInput.value.trim()) return
//
chatHistory.value.push({
role: 'user',
content: userInput.value,
})
// AIAPI
setTimeout(() => {
if (activeAgent.value === 'crypto-doctor') {
chatHistory.value.push({
role: 'assistant',
content:
'我正在分析这个项目,这可能需要一些时间。基于初步分析,我建议关注以下几个方面:\n\n1. 团队背景和透明度\n2. 代码审计情况\n3. 社区活跃度和增长\n4. 代币分配机制\n5. 市场流动性\n\n您想了解哪方面的详细信息',
})
} else {
chatHistory.value.push({
role: 'assistant',
content:
'根据最近的市场数据和技术指标分析,我注意到该代币的趋势如下:\n\n- 价格走势:呈现上升通道,但接近阻力位\n- 交易量:逐渐增加,表明兴趣上升\n- RSI指标当前处于60左右未进入超买区域\n- MACD短期均线刚刚穿过长期均线形成黄金交叉\n\n需要注意的是市场可能受到宏观经济因素的影响。您希望了解更多具体的技术指标分析吗',
})
}
userInput.value = ''
}, 1000)
}
const switchAgent = (agent: string) => {
activeAgent.value = agent
chatHistory.value = [
{
role: 'assistant',
content:
agent === 'crypto-doctor'
? '你好!我是加密项目医生,可以帮你分析任何加密项目的健康状况。请告诉我你想了解的项目名称或合约地址。'
: '你好!我是币种技术分析师,可以为你提供币种的技术面分析和趋势预测。请告诉我你想分析的币种名称或代号。',
},
]
}
</script>
<template>
<div class="ai-agent-view">
<h1 class="page-title">AI Agent</h1>
<p class="page-description">智能AI助手为您提供加密项目分析和技术指标解读</p>
<div class="agent-container">
<div class="agent-sidebar">
<div class="agent-selector">
<button
class="agent-option"
:class="{ active: activeAgent === 'crypto-doctor' }"
@click="switchAgent('crypto-doctor')"
>
<span class="agent-icon">🔍</span>
<div class="agent-info">
<h3>加密项目医生</h3>
<p>项目风险分析与评估</p>
</div>
</button>
<button
class="agent-option"
:class="{ active: activeAgent === 'technical-analysis' }"
@click="switchAgent('technical-analysis')"
>
<span class="agent-icon">📊</span>
<div class="agent-info">
<h3>币种技术分析</h3>
<p>技术指标与趋势预测</p>
</div>
</button>
</div>
<div class="sidebar-info">
<h3>使用提示</h3>
<ul class="tips-list">
<li>提供详细的项目信息以获得更准确的分析</li>
<li>可以直接输入合约地址进行分析</li>
<li>分析结果仅供参考不构成投资建议</li>
</ul>
</div>
</div>
<div class="chat-container">
<div class="chat-header">
<h2>{{ activeAgent === 'crypto-doctor' ? '加密项目医生' : '币种技术分析' }}</h2>
</div>
<div class="chat-messages">
<div
v-for="(message, index) in chatHistory"
:key="index"
class="message"
:class="{
'user-message': message.role === 'user',
'ai-message': message.role === 'assistant',
}"
>
<div class="message-content">
<p v-for="(line, i) in message.content.split('\n')" :key="i">
{{ line }}
</p>
</div>
</div>
</div>
<div class="chat-input">
<input
type="text"
v-model="userInput"
@keyup.enter="sendMessage"
placeholder="输入项目名称、合约地址或问题..."
class="input-field"
/>
<button class="send-button" @click="sendMessage">发送</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.ai-agent-view {
width: 100%;
padding: 0;
}
.page-title {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.page-description {
color: var(--color-text-secondary);
margin-bottom: 2rem;
}
.agent-container {
display: flex;
gap: 1.5rem;
height: calc(80vh - 100px);
min-height: 500px;
width: 100%;
}
.agent-sidebar {
width: 300px;
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
border: 1px solid var(--color-border);
}
.agent-selector {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.agent-option {
display: flex;
align-items: center;
gap: 0.8rem;
padding: 1rem;
border-radius: var(--border-radius);
background-color: var(--color-bg-primary);
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
}
.agent-option:hover {
border-color: var(--color-border);
}
.agent-option.active {
border-color: var(--color-accent);
background-color: var(--color-accent-light);
}
.agent-icon {
font-size: 1.8rem;
}
.agent-info h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.2rem;
color: rgba(255, 255, 255, 0.9);
}
.agent-info p {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.agent-option.active .agent-info h3 {
color: var(--color-accent-hover);
}
.sidebar-info {
margin-top: auto;
padding-top: 1rem;
border-top: 1px solid var(--color-border);
}
.sidebar-info h3 {
font-size: 1rem;
margin-bottom: 0.8rem;
}
.tips-list {
list-style-type: none;
padding: 0;
}
.tips-list li {
color: var(--color-text-secondary);
font-size: 0.9rem;
margin-bottom: 0.5rem;
padding-left: 1rem;
position: relative;
}
.tips-list li::before {
content: '•';
position: absolute;
left: 0;
color: var(--color-accent);
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
overflow: hidden;
border: 1px solid var(--color-border);
}
.chat-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.chat-header h2 {
font-size: 1.2rem;
font-weight: 600;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 80%;
padding: 1rem;
border-radius: var(--border-radius);
position: relative;
}
.user-message {
align-self: flex-end;
background-color: var(--color-accent);
color: white;
}
.ai-message {
align-self: flex-start;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
}
.message-content p {
margin-bottom: 0.5rem;
}
.message-content p:last-child {
margin-bottom: 0;
}
.chat-input {
display: flex;
padding: 1rem 1.5rem;
border-top: 1px solid var(--color-border);
}
.input-field {
flex: 1;
padding: 0.8rem 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--color-border);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 1rem;
}
.input-field:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 2px var(--color-accent-light);
outline: none;
}
.send-button {
background-color: var(--color-accent);
color: var(--color-bg-primary);
border: none;
border-radius: var(--border-radius);
padding: 0 1.2rem;
margin-left: 0.8rem;
font-weight: var(--font-weight-bold);
cursor: pointer;
transition: background-color 0.2s ease;
}
.send-button:hover {
background-color: var(--color-accent-hover);
}
.btn-connect,
.btn-primary,
.btn-secondary {
color: var(--color-bg-primary);
font-weight: var(--font-weight-medium);
}
@media (max-width: 1024px) {
.agent-container {
flex-direction: row;
flex-wrap: wrap;
}
.agent-sidebar {
width: 250px;
}
.chat-container {
flex-basis: calc(100% - 270px);
}
}
@media (max-width: 768px) {
.agent-container {
flex-direction: column;
height: auto;
}
.agent-sidebar {
width: 100%;
height: auto;
padding: 1rem;
}
.agent-selector {
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.agent-option {
flex: 1;
min-width: 140px;
flex-direction: column;
text-align: center;
}
.agent-info h3 {
font-size: 0.9rem;
}
.agent-info p {
display: none;
}
.sidebar-info {
display: none;
}
.chat-container {
height: 70vh;
flex-basis: 100%;
}
}
</style>

15
src/views/AboutView.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

716
src/views/CommunityView.vue Normal file
View File

@ -0,0 +1,716 @@
<script setup lang="ts">
import { ref } from 'vue'
interface Post {
id: number
author: {
name: string
avatar: string
verified: boolean
}
content: string
timestamp: string
likes: number
comments: number
reposts: number
isLiked: boolean
images?: string[]
tags?: string[]
}
const newPostContent = ref('')
const filter = ref('for-you')
const posts = ref<Post[]>([
{
id: 1,
author: {
name: 'CryptoInsight',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=CryptoInsight',
verified: true,
},
content:
'比特币减半已经完成!这对市场来说是一个重要的里程碑。你们认为这会对价格产生什么影响? #比特币 #减半 #加密货币',
timestamp: '1小时前',
likes: 128,
comments: 43,
reposts: 21,
isLiked: false,
tags: ['比特币', '减半', '加密货币'],
},
{
id: 2,
author: {
name: 'ETH_Developer',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=ETH_Developer',
verified: true,
},
content: '以太坊过去24小时的Gas费用达到了近期最低点。现在是进行链上交易的好时机',
timestamp: '3小时前',
likes: 76,
comments: 12,
reposts: 8,
isLiked: true,
tags: ['以太坊', 'Gas费用'],
},
{
id: 3,
author: {
name: 'DeFi_Master',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=DeFi_Master',
verified: false,
},
content:
'刚刚研究了一个新的DeFi项目提供了非常具有竞争力的流动性挖矿回报。大家有兴趣了解更多吗',
timestamp: '5小时前',
likes: 54,
comments: 31,
reposts: 4,
isLiked: false,
},
{
id: 4,
author: {
name: 'NFT_Collector',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=NFT_Collector',
verified: true,
},
content:
'刚刚收购了一个稀有的蓝筹NFT这是我收藏的最新成员。艺术和区块链的结合真的很令人兴奋',
timestamp: '6小时前',
likes: 92,
comments: 14,
reposts: 7,
isLiked: false,
images: ['https://picsum.photos/id/29/600/400'],
},
{
id: 5,
author: {
name: 'Crypto_News',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=Crypto_News',
verified: true,
},
content:
'重大新闻SEC批准了第一个比特币现货ETF这对加密市场的监管和机构采用是一个重要的进展。 #比特币ETF #SEC #加密监管',
timestamp: '12小时前',
likes: 215,
comments: 87,
reposts: 56,
isLiked: true,
tags: ['比特币ETF', 'SEC', '加密监管'],
},
])
const toggleLike = (postId: number) => {
const post = posts.value.find((p) => p.id === postId)
if (post) {
post.isLiked = !post.isLiked
post.likes += post.isLiked ? 1 : -1
}
}
const createPost = () => {
if (!newPostContent.value.trim()) return
const newPost: Post = {
id: posts.value.length + 1,
author: {
name: '当前用户',
avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=CurrentUser',
verified: false,
},
content: newPostContent.value,
timestamp: '刚刚',
likes: 0,
comments: 0,
reposts: 0,
isLiked: false,
}
posts.value.unshift(newPost)
newPostContent.value = ''
}
</script>
<template>
<div class="community-view">
<h1 class="page-title">社区</h1>
<p class="page-description">与加密货币爱好者交流分享见解和最新动态</p>
<div class="community-layout">
<div class="main-content">
<div class="feed-tabs">
<button
class="tab-btn"
:class="{ active: filter === 'for-you' }"
@click="filter = 'for-you'"
>
为你推荐
</button>
<button
class="tab-btn"
:class="{ active: filter === 'following' }"
@click="filter = 'following'"
>
关注
</button>
<button
class="tab-btn"
:class="{ active: filter === 'trending' }"
@click="filter = 'trending'"
>
热门话题
</button>
</div>
<div class="post-creator">
<div class="post-input-container">
<img
src="https://api.dicebear.com/7.x/shapes/svg?seed=CurrentUser"
alt="User Avatar"
class="user-avatar"
/>
<textarea
v-model="newPostContent"
placeholder="分享你的加密见解..."
class="post-input"
></textarea>
</div>
<div class="post-actions">
<div class="post-tools">
<button class="tool-btn">📷</button>
<button class="tool-btn">🔗</button>
<button class="tool-btn">#</button>
</div>
<button
class="btn btn-primary post-btn"
@click="createPost"
:disabled="!newPostContent.trim()"
>
发布
</button>
</div>
</div>
<div class="posts-feed">
<div v-for="post in posts" :key="post.id" class="post-card">
<div class="post-header">
<img :src="post.author.avatar" alt="User Avatar" class="post-avatar" />
<div class="post-author">
<div class="author-name">
{{ post.author.name }}
<span v-if="post.author.verified" class="verified-badge"></span>
</div>
<span class="post-time">{{ post.timestamp }}</span>
</div>
</div>
<div class="post-content">
<p>{{ post.content }}</p>
<div v-if="post.images && post.images.length" class="post-images">
<img
v-for="(img, index) in post.images"
:key="index"
:src="img"
alt="Post image"
class="post-image"
/>
</div>
<div v-if="post.tags && post.tags.length" class="post-tags">
<span v-for="(tag, index) in post.tags" :key="index" class="post-tag">
#{{ tag }}
</span>
</div>
</div>
<div class="post-footer">
<button
class="post-action"
:class="{ active: post.isLiked }"
@click="toggleLike(post.id)"
>
<span class="action-icon"></span>
<span class="action-count">{{ post.likes }}</span>
</button>
<button class="post-action">
<span class="action-icon">💬</span>
<span class="action-count">{{ post.comments }}</span>
</button>
<button class="post-action">
<span class="action-icon">🔄</span>
<span class="action-count">{{ post.reposts }}</span>
</button>
<button class="post-action">
<span class="action-icon">📤</span>
</button>
</div>
</div>
</div>
</div>
<div class="sidebar">
<div class="sidebar-section">
<h3 class="sidebar-title">热门话题</h3>
<ul class="trending-list">
<li class="trending-item">
<div class="trending-tag">#比特币减半</div>
<div class="trending-stats">12.5K 帖子</div>
</li>
<li class="trending-item">
<div class="trending-tag">#以太坊2.0</div>
<div class="trending-stats">8.3K 帖子</div>
</li>
<li class="trending-item">
<div class="trending-tag">#DeFi夏天</div>
<div class="trending-stats">6.7K 帖子</div>
</li>
<li class="trending-item">
<div class="trending-tag">#NFT艺术</div>
<div class="trending-stats">5.2K 帖子</div>
</li>
<li class="trending-item">
<div class="trending-tag">#Layer2扩容</div>
<div class="trending-stats">3.9K 帖子</div>
</li>
</ul>
</div>
<div class="sidebar-section">
<h3 class="sidebar-title">推荐关注</h3>
<ul class="suggested-users">
<li class="user-item">
<img
src="https://api.dicebear.com/7.x/shapes/svg?seed=VitalikB"
alt="User Avatar"
class="user-avatar-small"
/>
<div class="user-info">
<div class="user-name">
Vitalik.eth
<span class="verified-badge-small"></span>
</div>
<div class="user-handle">@VitalikButerin</div>
</div>
<button class="btn btn-secondary btn-small">关注</button>
</li>
<li class="user-item">
<img
src="https://api.dicebear.com/7.x/shapes/svg?seed=SBF"
alt="User Avatar"
class="user-avatar-small"
/>
<div class="user-info">
<div class="user-name">CZ Binance</div>
<div class="user-handle">@cz_binance</div>
</div>
<button class="btn btn-secondary btn-small">关注</button>
</li>
<li class="user-item">
<img
src="https://api.dicebear.com/7.x/shapes/svg?seed=ElonM"
alt="User Avatar"
class="user-avatar-small"
/>
<div class="user-info">
<div class="user-name">
Elon Musk
<span class="verified-badge-small"></span>
</div>
<div class="user-handle">@elonmusk</div>
</div>
<button class="btn btn-secondary btn-small">关注</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.community-view {
width: 100%;
max-width: 1440px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 350px;
gap: 2rem;
padding: 0 1rem;
}
.page-title {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.page-description {
color: var(--color-text-secondary);
margin-bottom: 2rem;
}
.community-layout {
display: flex;
flex-wrap: wrap;
gap: 2rem;
width: 100%;
}
.main-content {
flex: 1;
min-width: 0;
flex-basis: 65%;
}
.feed-tabs {
display: flex;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border);
width: 100%;
overflow-x: auto;
}
.tab-btn {
background: transparent;
border: none;
padding: 1rem 1.5rem;
font-size: 1rem;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
position: relative;
white-space: nowrap;
}
.tab-btn:hover {
color: var(--color-text-primary);
}
.tab-btn.active {
color: var(--color-accent);
font-weight: 600;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 2px;
background-color: var(--color-accent);
}
.post-creator {
background-color: var(--color-bg-card);
padding: 1.5rem;
border-radius: var(--border-radius);
margin-bottom: 2rem;
border: 1px solid var(--color-border);
}
.post-input-container {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.user-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
flex-shrink: 0;
}
.post-input {
flex: 1;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: 0.8rem;
color: var(--color-text-primary);
font-size: 1rem;
min-height: 100px;
resize: vertical;
}
.post-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.post-tools {
display: flex;
gap: 0.5rem;
}
.tool-btn {
background-color: transparent;
border: none;
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s ease;
font-size: 1.2rem;
}
.tool-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.post-btn {
padding: 0.6rem 1.5rem;
}
.posts-feed {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.post-card {
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
padding: 1.5rem;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
}
.post-card:hover {
border-color: var(--color-accent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.post-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.post-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
}
.post-author {
flex: 1;
}
.author-name {
font-weight: 600;
display: flex;
align-items: center;
gap: 0.3rem;
}
.verified-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background-color: var(--color-accent);
color: white;
border-radius: 50%;
font-size: 0.7rem;
}
.post-time {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.post-content {
margin-bottom: 1.5rem;
line-height: 1.5;
}
.post-content p {
margin-bottom: 1rem;
}
.post-images {
margin-top: 1rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
border-radius: var(--border-radius);
overflow: hidden;
}
.post-image {
max-width: 100%;
height: auto;
object-fit: cover;
flex: 1;
min-width: 200px;
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.post-tag {
color: var(--color-accent);
font-size: 0.9rem;
cursor: pointer;
}
.post-tag:hover {
text-decoration: underline;
}
.post-footer {
display: flex;
gap: 1.5rem;
}
.post-action {
background-color: transparent;
border: none;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--color-text-secondary);
cursor: pointer;
transition: color 0.2s ease;
}
.post-action:hover,
.post-action.active {
color: var(--color-accent);
}
.action-icon {
font-size: 1.2rem;
}
/* Sidebar Styling */
.sidebar {
width: 300px;
position: sticky;
top: calc(var(--header-height) + 1rem);
align-self: flex-start;
}
.sidebar-section {
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--color-border);
}
.sidebar-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 1.2rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid var(--color-border);
}
.trending-list,
.suggested-users {
list-style: none;
padding: 0;
}
.trending-item {
padding: 0.8rem 0;
border-bottom: 1px solid var(--color-border);
}
.trending-item:last-child {
border-bottom: none;
}
.trending-tag {
font-weight: 600;
margin-bottom: 0.3rem;
}
.trending-stats {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.user-item {
display: flex;
align-items: center;
gap: 0.8rem;
padding: 0.8rem 0;
border-bottom: 1px solid var(--color-border);
}
.user-item:last-child {
border-bottom: none;
}
.user-avatar-small {
width: 40px;
height: 40px;
border-radius: 50%;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: 600;
display: flex;
align-items: center;
gap: 0.3rem;
}
.verified-badge-small {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
background-color: var(--color-accent);
color: white;
border-radius: 50%;
font-size: 0.6rem;
}
.user-handle {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.btn-small {
padding: 0.3rem 0.8rem;
font-size: 0.9rem;
}
@media (max-width: 1200px) {
.community-view {
grid-template-columns: 1fr 300px;
gap: 1.5rem;
}
}
@media (max-width: 980px) {
.community-view {
grid-template-columns: 1fr;
gap: 1rem;
}
.sidebar {
display: none;
}
}
</style>

336
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,336 @@
<script setup lang="ts"></script>
<template>
<div class="home-view">
<section class="hero-section">
<div class="hero-content">
<h1 class="hero-title">Crypto.AI <span class="accent">加密货币工具平台</span></h1>
<p class="hero-subtitle">一站式加密货币工具集合AI分析和社区互动平台</p>
<div class="hero-actions">
<button class="btn btn-primary">探索工具</button>
<button class="btn btn-secondary">加入社区</button>
</div>
</div>
</section>
<section class="features-section">
<div class="feature-card card">
<div class="feature-icon">🛠</div>
<h3 class="feature-title">工具集合</h3>
<p class="feature-desc">批量钱包创建钱包归集等实用工具助您高效管理加密资产</p>
<button class="btn-action">了解更多</button>
</div>
<div class="feature-card card">
<div class="feature-icon">🤖</div>
<h3 class="feature-title">AI Agent</h3>
<p class="feature-desc">加密项目医生与币种技术分析让您的投资决策更明智</p>
<button class="btn-action">了解更多</button>
</div>
<div class="feature-card card">
<div class="feature-icon">👥</div>
<h3 class="feature-title">社区互动</h3>
<p class="feature-desc">类似微博的信息流实时了解行业动态分享您的见解</p>
<button class="btn-action">了解更多</button>
</div>
</section>
<section class="stats-section">
<div class="stats-header">
<h2>平台数据</h2>
<p>实时数据</p>
</div>
<div class="stats-grid">
<div class="stat-card card">
<div class="stat-value">10,000+</div>
<div class="stat-label">钱包已创建</div>
</div>
<div class="stat-card card">
<div class="stat-value">5,000+</div>
<div class="stat-label">项目已分析</div>
</div>
<div class="stat-card card">
<div class="stat-value">20,000+</div>
<div class="stat-label">社区成员</div>
</div>
<div class="stat-card card">
<div class="stat-value">99.9%</div>
<div class="stat-label">正常运行时间</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.home-view {
width: 100%;
}
.hero-section {
text-align: center;
padding: 4rem 2rem;
margin-bottom: 4rem;
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius);
position: relative;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
width: 100%;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.1), transparent 50%);
}
.hero-content {
position: relative;
z-index: 1;
max-width: 900px;
margin: 0 auto;
}
.hero-title {
font-size: 3.5rem;
font-weight: 800;
margin-bottom: 1.5rem;
line-height: 1.2;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.accent {
color: var(--color-accent);
position: relative;
display: inline-block;
}
.accent::after {
content: '';
position: absolute;
bottom: 5px;
left: 0;
width: 100%;
height: 8px;
background-color: var(--color-accent-light);
z-index: -1;
border-radius: 4px;
}
.hero-subtitle {
font-size: 1.2rem;
color: var(--color-text-secondary);
max-width: 700px;
margin: 0 auto 2.5rem;
}
.hero-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
padding: 0.8rem 1.6rem;
border-radius: 6px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.btn-primary {
background-color: var(--color-accent);
color: var(--color-bg-primary);
font-weight: var(--font-weight-bold);
}
.btn-primary:hover {
background-color: var(--color-accent-hover);
color: var(--color-bg-primary);
}
.btn-secondary {
background-color: transparent;
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.features-section {
display: flex;
flex-wrap: wrap;
gap: 2rem;
margin-bottom: 4rem;
width: 100%;
}
.feature-card {
flex: 1;
min-width: 300px;
padding: 2.5rem;
display: flex;
flex-direction: column;
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
border: 1px solid var(--color-border);
height: 100%;
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
}
.feature-icon {
font-size: 3rem;
margin-bottom: 1.5rem;
background-color: var(--color-accent-light);
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 15px;
}
.feature-title {
font-size: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
}
.feature-desc {
color: var(--color-text-secondary);
margin-bottom: 1.5rem;
line-height: 1.5;
flex-grow: 1;
}
.btn-action {
align-self: flex-start;
background: transparent;
color: var(--color-accent);
border: none;
padding: 0.5rem 0;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s ease;
}
.btn-action::after {
content: '→';
margin-left: 0.5rem;
transition: transform 0.2s ease;
}
.btn-action:hover::after {
transform: translateX(4px);
}
.stats-section {
margin-bottom: 4rem;
width: 100%;
}
.stats-header {
text-align: center;
margin-bottom: 2rem;
}
.stats-header h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.stats-header p {
color: var(--color-text-secondary);
}
.stats-grid {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
width: 100%;
}
.stat-card {
flex: 1;
min-width: 240px;
text-align: center;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid var(--color-border);
background-color: var(--color-bg-card);
border-radius: var(--border-radius);
}
.stat-card:hover {
border-color: var(--color-accent);
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--color-accent);
margin-bottom: 0.5rem;
}
.stat-label {
color: var(--color-text-secondary);
font-size: 1.1rem;
}
@media (max-width: 980px) {
.feature-card {
min-width: calc(50% - 1rem);
}
}
@media (max-width: 768px) {
.hero-title {
font-size: 2.5rem;
}
.hero-actions {
flex-direction: column;
align-items: center;
}
.feature-card {
min-width: 100%;
}
.stat-card {
min-width: calc(50% - 1rem);
}
}
@media (max-width: 480px) {
.stat-card {
min-width: 100%;
}
.hero-section {
padding: 3rem 1rem;
}
.hero-title {
font-size: 2.2rem;
}
}
</style>

392
src/views/ToolsView.vue Normal file
View File

@ -0,0 +1,392 @@
<script setup lang="ts">
import { ref } from 'vue'
const activeTab = ref('wallet-creation')
</script>
<template>
<div class="tools-view">
<div class="page-header">
<h1 class="page-title">工具集合</h1>
<p class="page-description">一系列实用的加密货币工具助您高效管理数字资产</p>
</div>
<div class="tools-tabs">
<button
class="tab-button"
:class="{ active: activeTab === 'wallet-creation' }"
@click="activeTab = 'wallet-creation'"
>
批量钱包创建
</button>
<button
class="tab-button"
:class="{ active: activeTab === 'wallet-collection' }"
@click="activeTab = 'wallet-collection'"
>
钱包归集
</button>
<button
class="tab-button"
:class="{ active: activeTab === 'other-tools' }"
@click="activeTab = 'other-tools'"
>
其他工具
</button>
</div>
<div class="tool-content card">
<!-- 批量钱包创建工具 -->
<div v-if="activeTab === 'wallet-creation'" class="tool-panel">
<h2 class="tool-title">批量钱包创建</h2>
<p class="tool-desc">批量创建以太坊兼容的钱包地址和私钥</p>
<div class="tool-form">
<div class="form-group">
<label>创建钱包数量</label>
<input
type="number"
class="form-input"
placeholder="输入需要创建的钱包数量"
min="1"
max="100"
value="10"
/>
</div>
<div class="form-group">
<label>钱包类型</label>
<select class="form-select">
<option value="eth">以太坊 (ETH)</option>
<option value="bsc">币安智能链 (BSC)</option>
<option value="polygon">Polygon</option>
<option value="arbitrum">Arbitrum</option>
</select>
</div>
<div class="form-group">
<label>导出格式</label>
<select class="form-select">
<option value="json">JSON</option>
<option value="csv">CSV</option>
<option value="txt">TXT</option>
</select>
</div>
<button class="btn btn-primary btn-block">开始创建</button>
</div>
<div class="results-preview">
<h3>预览结果</h3>
<div class="preview-content">
<p>暂无数据请先创建钱包</p>
</div>
</div>
</div>
<!-- 钱包归集工具 -->
<div v-if="activeTab === 'wallet-collection'" class="tool-panel">
<h2 class="tool-title">钱包归集</h2>
<p class="tool-desc">将多个钱包中的资产归集到一个目标钱包</p>
<div class="tool-form">
<div class="form-group">
<label>目标钱包地址</label>
<input type="text" class="form-input" placeholder="输入归集目标钱包地址" />
</div>
<div class="form-group">
<label>源钱包私钥列表</label>
<textarea class="form-textarea" placeholder="每行输入一个私钥..."></textarea>
</div>
<div class="form-group">
<label>网络</label>
<select class="form-select">
<option value="eth">以太坊 (ETH)</option>
<option value="bsc">币安智能链 (BSC)</option>
<option value="polygon">Polygon</option>
<option value="arbitrum">Arbitrum</option>
</select>
</div>
<div class="form-group">
<label>GAS设置</label>
<div class="gas-settings">
<input type="text" class="form-input" placeholder="Gas Price (Gwei)" />
<input type="text" class="form-input" placeholder="Gas Limit" value="21000" />
</div>
</div>
<button class="btn btn-primary btn-block">开始归集</button>
</div>
<div class="results-preview">
<h3>归集状态</h3>
<div class="preview-content">
<p>暂无数据请先开始归集</p>
</div>
</div>
</div>
<!-- 其他工具 -->
<div v-if="activeTab === 'other-tools'" class="tool-panel">
<h2 class="tool-title">其他工具</h2>
<p class="tool-desc">更多实用的加密货币工具</p>
<div class="tools-grid">
<div class="tool-card card">
<h3>Token授权查询</h3>
<p>查询并撤销钱包中的Token授权</p>
<button class="btn btn-secondary">使用工具</button>
</div>
<div class="tool-card card">
<h3>交易解码</h3>
<p>解码链上交易数据了解交易详情</p>
<button class="btn btn-secondary">使用工具</button>
</div>
<div class="tool-card card">
<h3>Gas计算器</h3>
<p>计算不同网络的Gas费用</p>
<button class="btn btn-secondary">使用工具</button>
</div>
<div class="tool-card card">
<h3>Merkle Tree生成器</h3>
<p>为空投活动生成Merkle Tree</p>
<button class="btn btn-secondary">使用工具</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.tools-view {
width: 100%;
}
.page-header {
margin-bottom: 2rem;
width: 100%;
}
.page-title {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.page-description {
color: var(--color-text-secondary);
}
.tools-tabs {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
background-color: var(--color-bg-secondary);
padding: 0.5rem;
border-radius: var(--border-radius);
width: 100%;
overflow-x: auto;
}
.tab-button {
background: transparent;
border: none;
color: var(--color-text-secondary);
padding: 0.8rem 1.5rem;
font-size: 1rem;
cursor: pointer;
border-radius: var(--border-radius);
transition: all 0.2s ease;
font-weight: 500;
white-space: nowrap;
}
.tab-button:hover {
color: var(--color-text-primary);
background-color: var(--color-bg-elevated);
}
.tab-button.active {
color: var(--color-text-primary);
background-color: var(--color-bg-card);
font-weight: 600;
}
.tool-content {
background-color: var(--color-bg-card);
border: 1px solid var(--color-border);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 100%;
}
.tool-panel {
padding: 2rem;
width: 100%;
}
.tool-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.tool-desc {
color: var(--color-text-secondary);
margin-bottom: 2rem;
}
.tool-form {
margin-bottom: 2rem;
max-width: 800px;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 0.8rem 1rem;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
color: var(--color-text-primary);
font-size: 1rem;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 2px var(--color-accent-light);
outline: none;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.gas-settings {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.gas-settings .form-input {
flex: 1;
min-width: 200px;
}
.btn-block {
width: 100%;
}
.results-preview {
background-color: var(--color-bg-primary);
padding: 1.5rem;
border-radius: var(--border-radius);
border: 1px solid var(--color-border);
max-width: 800px;
}
.results-preview h3 {
margin-bottom: 1rem;
font-size: 1.2rem;
font-weight: 600;
}
.preview-content {
color: var(--color-text-secondary);
min-height: 100px;
}
.tools-grid {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
width: 100%;
}
.tool-card {
flex: 1;
min-width: 280px;
background-color: var(--color-bg-primary);
padding: 1.5rem;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
border-radius: var(--border-radius);
}
.tool-card:hover {
border-color: var(--color-accent);
transform: translateY(-3px);
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.2);
}
.tool-card h3 {
font-size: 1.2rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
.tool-card p {
color: var(--color-text-secondary);
margin-bottom: 1.2rem;
font-size: 0.9rem;
flex-grow: 1;
}
@media (max-width: 980px) {
.tool-card {
min-width: calc(50% - 1rem);
}
}
@media (max-width: 768px) {
.tools-tabs {
flex-direction: column;
gap: 0.5rem;
}
.tab-button {
text-align: left;
border-radius: var(--border-radius);
}
.tool-card {
min-width: 100%;
}
.tool-panel {
padding: 1.5rem 1rem;
}
}
@media (max-width: 480px) {
.page-title {
font-size: 1.8rem;
}
}
</style>

12
tsconfig.app.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

14
tsconfig.json Normal file
View File

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

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

11
tsconfig.vitest.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.app.json",
"include": ["src/**/__tests__/*", "env.d.ts"],
"exclude": [],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"lib": [],
"types": ["node", "jsdom"]
}
}

20
vite.config.ts Normal file
View File

@ -0,0 +1,20 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})

14
vitest.config.ts Normal file
View File

@ -0,0 +1,14 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)