first commit

This commit is contained in:
lqs 2024-08-26 08:05:26 +00:00
commit 7c78a5bb35
71 changed files with 4712 additions and 0 deletions

238
README.md Normal file
View File

@ -0,0 +1,238 @@
# APP
## PATH
### UI
### Server
#### thirdparty
##### wkhtmltopdf
#### addons
##### base
###### database
* data
* i18n
* models
* res.partner
* ir.module.module
* security
###### alembic
##### .....
##### ......
#### app.log
#### app.conf
#### requirements.txt
#### main.py
### venv
### README.md
### setup
#### 启动服务器
#### 关闭服务器
#### 初始化迁移
#### 迁移脚本
#### 执行迁移
## PATH
### UI
### Server
#### thirdparty
##### wkhtmltopdf
#### addons
##### base
###### database
* data
* i18n
* models
* res.partner
* ir.module.module
* security
###### alembic
##### .....
##### ......
#### app.log
#### app.conf
#### requirements.txt
#### main.py
### venv
### README.md
### setup
#### 启动服务器
#### 关闭服务器
#### 初始化迁移
#### 迁移脚本
#### 执行迁移
## pip
### fastapi
#### app = FastAPI()
##### 模块创建应用实例
#### Header
##### 类型注解获取请求头
#### Cookie
##### Cookie 数据
#### responses
##### RedirectResponse
###### 实现重定向
#### HTTPException
##### 返回状态码
#### JSONResponse
##### 自定义响应头
#### Query
##### 验证查询参数、路径参数
#### Depends
##### 预处理 后处理
#### Form
##### 表单上传
#### UploadFile
##### 文件上传
### uvicorn[standard]
#### httptools
##### 处理请求和响应
#### h11
##### 扩展http交互
#### uvloop
##### 异步循环事件
#### websockets
##### 全双工
### python-multipart
#### 表单校验
### pydantic
#### from pydantic import BaseModel
##### 用于数据验证和序列化
#### Field
##### 表单字段定义
### asyncio
#### 异步
### typing
#### Optional
##### 指定必须数据类型
#### Union
##### 支持多种数据类型
### sqlalchemy
#### 操作数据库
### passlib
#### 密码算法
### pyJWT
#### 鉴权验证
### alembic
#### 数据库迁移
## command
### git init
### git add .
### git commit -m "lqs"
### git remote add origin http://8.134.237.70:2023/lqs/ST5-fastapi-vue3.git
### git push -u origin main

BIN
command.xmind Normal file

Binary file not shown.

49
server/.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# sphinx build directories
_build/
# dotfiles
.*
!.gitignore
!.github
!.mailmap
# compiled python files
*.py[co]
__pycache__/
# setup.py egg_info
*.egg-info
# emacs backup files
*~
# hg stuff
*.orig
status
# server filestore
server/filestore
# maintenance migration scripts
server/addons/base/maintenance
# generated for windows installer?
install/win32/*.bat
install/win32/meta.py
# needed only when building for win32
setup/win32/static/less/
setup/win32/static/wkhtmltopdf/
setup/win32/static/postgresql*.exe
# js tooling
node_modules
jsconfig.json
tsconfig.json
package-lock.json
package.json
.husky
# various virtualenv
/bin/
/build/
/dist/
/include/
/lib/
/man/
/share/
/src/

BIN
server/PATH_Server.xmind Normal file

Binary file not shown.

BIN
server/PIP_Server.xmind Normal file

Binary file not shown.

1
server/README.md Normal file
View File

@ -0,0 +1 @@
uvicorn server.main:app --reload

BIN
server/Route_Server.xmind Normal file

Binary file not shown.

17
server/__init__.py Normal file
View File

@ -0,0 +1,17 @@
import os
# 获取当前文件的绝对路径
current_file_path = os.path.abspath(__file__)
# 获取项目根目录的路径
project_root_path = os.path.dirname(os.path.dirname(current_file_path))
# 将项目根目录路径作为一个全局变量提供
PROJECT_ROOT_PATH = project_root_path
# 设置日志配置
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("测试打印项目根路径: %s", PROJECT_ROOT_PATH)

View File

View File

View File

View File

@ -0,0 +1,49 @@
import os
import configparser
import logging
# Assuming PROJECT_ROOT_PATH is defined elsewhere in your project
# If not, define it here
# PROJECT_ROOT_PATH = ...
# Import the PROJECT_ROOT_PATH
from .... import PROJECT_ROOT_PATH
logger = logging.getLogger(__name__)
# Construct the full path to the configuration file
server_path = os.path.join(PROJECT_ROOT_PATH, 'server', 'server.conf')
config_parser = configparser.ConfigParser()
config_parser.read(server_path)
try:
host = config_parser.get('options', 'db_host')
port = config_parser.getint('options', 'db_port')
username = config_parser.get('options', 'db_user')
password = config_parser.get('options', 'db_password')
database = config_parser.get('options', 'db_name')
sqlname = config_parser.get('options', 'db_type')
except configparser.NoOptionError as e:
logger.error(f"Configuration error: {e}")
raise
# Ensure that the password and other fields are properly encoded
# This step is optional if you are sure that the password does not contain non-ASCII characters
# password = password.encode('utf-8').decode('unicode_escape')
DATABASE_URL = f"{sqlname}://{username}:{password}@{host}:{port}/{database}"
# Optionally, log the constructed URL (for debugging purposes)
logger.info("Constructed DATABASE_URL: %s", DATABASE_URL)
class Config:
def __init__(self, config_file=server_path):
self.config = configparser.ConfigParser()
self.config.read(config_file)
def get(self, section, option):
return self.config.get(section, option)
def getint(self, section, option):
return self.config.getint(section, option)
logger.info("配置文件路径: %s", server_path)
logger.info("链接信息: %s", DATABASE_URL)

View File

@ -0,0 +1,121 @@
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .configparser import DATABASE_URL
import logging
import psycopg2
from psycopg2 import sql
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from ....main import app
from sqlalchemy.orm import Session
from pydantic import ValidationError
router = APIRouter()
from passlib.context import CryptContext
logger = logging.getLogger(__name__)
Base = declarative_base()
#哈希功能
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password):
return pwd_context.hash(password)
class ResPartner(Base):
__tablename__ = 'res.artner'
id = Column(Integer, primary_key=True)
password = Column(String, nullable=False)
phone = Column(String, nullable=False)
language = Column(String, nullable=False)
region = Column(String, nullable=False)
class ResPartnerValidationModel(BaseModel):
masterPassword: str
databaseName: str
email: str
password: str
phoneNumber: str
selectedLanguage: str
selectedCountry: str
selectedDemoData: str
# masterPassword: masterPassword.value,
# databaseName: databaseName.value,
# email: email.value,
# password: password.value,
# phoneNumber: phoneNumber.value,
# selectedLanguage: selectedLanguage.value,
# selectedCountry: selectedCountry.value,
# selectedDemoData: selectedDemoData.value,
# 创建数据库引擎
engine = create_engine(DATABASE_URL + '?client_encoding=utf8') # 显式指定编码
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
logger.info("数据库连接关闭")
@router.post("/manager/create", response_model=dict)
def create_database(response_data_model: ResPartnerValidationModel, db: Session = Depends(get_db)):
conn = None
createdbname = response_data_model.databaseName
try:
conn = psycopg2.connect(DATABASE_URL)
conn.autocommit = True # 设置自动提交模式
cursor = conn.cursor()
# 使用 sql.SQL 和 sql.Identifier 安全地构建SQL语句
query = sql.SQL("CREATE DATABASE {}").format(sql.Identifier(createdbname))
cursor.execute(query)
logger.info("创建数据库成功",{createdbname})
create_res = ResPartner(
name=response_data_model.name,
username=response_data_model.username,
password=get_password_hash(response_data_model.password)
)
db.add(create_res)
db.commit()
db.refresh(create_res)
# masterPassword: str
# databaseName: str
# email: str
# password: str
# phoneNumber: str
# selectedLanguage: str
# selectedCountry: str
# selectedDemoData: str
#自动创建全部表
return {"message": "Database created successfully"}
# except ValidationError as e:
# raise HTTPException(status_code=422, detail=e.errors())
except Exception as e:
logger.error("创建数据库失败: %s", e)
finally:
if conn is not None:
conn.close()
def create_teable():
Base.metadata.create_all(engine)
logger.info("创建全部表成功")
def setup_admin():
db = None
try:
db = SessionLocal()
admin = ResPartner(password='Sj89061189', phone='13929931775', language='zh-CN', region='China')
db.add(admin)
db.commit()
db.refresh(admin)
logger.info("创建超级管理员成功: %s", admin.id)
except Exception as e:
logger.error("创建超级管理员失败: %s", e)
if db is not None:
db.rollback()
finally:
if db is not None:
db.close()

View File

62
server/main.py Normal file
View File

@ -0,0 +1,62 @@
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from .addons.base.models.configparser import Config
from .addons.base.models.configparser import DATABASE_URL
import psycopg2
from psycopg2 import sql
from fastapi.responses import JSONResponse
import logging
from fastapi.middleware.cors import CORSMiddleware
origins = [
"*",
]
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"], # 允许所有 HTTP 方法
allow_headers=["*"], # 允许所有头部
)
config = Config()
def get_databases():
conn = None
try:
# 连接到默认的PostgreSQL数据库通常为postgres
conn = psycopg2.connect(DATABASE_URL) # 使用DATABASE_URL连接
conn.autocommit = True # 设置自动提交模式
cursor = conn.cursor()
# 查询所有数据库排除postgres
cursor.execute(sql.SQL("SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres';"))
databases = [row[0] for row in cursor.fetchall()]
logger.info("获取已有数据库列表: %s", databases)
return databases
except Exception as e:
logger.error("Error getting databases: %s", e)
return []
finally:
if conn is not None:
conn.close()
@app.get("/", response_class=JSONResponse)
async def get_databases_route():
databases = get_databases()
return databases
from .addons.base.models import database
app.include_router(database.router, prefix="/web/database", tags=["数据库"])
if __name__ == '__main__':
try:
server_port = config.getint('options', 'http_port')
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=server_port)
except Exception as e:
print(f"Failed to start the server: {e}")

12
server/requirements.txt Normal file
View File

@ -0,0 +1,12 @@
# pip install -r requirements.txt
fastapi
pydantic
asyncpg
sqlalchemy
python-multipart
uvicorn[standard]
psycopg2-binary
passlib
pyJWT
alembic

70
server/server.conf Normal file
View File

@ -0,0 +1,70 @@
[options]
# addons_path = c:\program files\server 16.0.20230716\server\server\addons
# admin_passwd = $pbkdf2-sha512$25000$QyglZAzBGGNsLYWwtnaudQ$veKyO6tHzwRapBc0j8J2Lw75qPXWM9jKpl8v2h6tx2owWxg99SASrXV0OjR6u8l6lBVKeS1k2BNNwDdocKcBnw
# bin_path = C:\Program Files\server 16.0.20230716\thirdparty
# csv_internal_sep = ,
# data_dir = C:\Program Files\server 16.0.20230716\sessions
db_host = localhost
# db_maxconn = 64
db_type = postgresql
db_name = postgres
db_password = Sj89061189
db_port = 5432
# db_sslmode = prefer
# db_template = template0
db_user = openpg
# dbfilter =
# default_productivity_apps = True
# demo = {}
# email_from = False
# from_filter = False
# geoip_database = c:\usr\share\geoip\geolite2-city.mmdb
# gevent_port = 8072
# http_enable = True
# http_interface =
http_port = 8000
# import_partial =
# limit_memory_hard = None
# limit_memory_soft = None
# limit_request = None
# limit_time_cpu = None
# limit_time_real = None
# limit_time_real_cron = None
# list_db = True
# log_db = False
# log_db_level = warning
# log_handler = :INFO
# log_level = info
# logfile = C:\Program Files\server 16.0.20230716\server\server.log
# max_cron_threads = 2
# osv_memory_age_limit = False
# osv_memory_count_limit = 0
# pg_path = C:\Program Files\server 16.0.20230716\PostgreSQL\bin
# pidfile =
# proxy_mode = False
# reportgz = False
# screencasts =
# screenshots = c:\users\administrator\appdata\local\temp\2\server_tests
# server_wide_modules = base,web
# smtp_password = False
# smtp_port = 25
# smtp_server = localhost
# smtp_ssl = False
# smtp_ssl_certificate_filename = False
# smtp_ssl_private_key_filename = False
# smtp_user = False
# syslog = False
# test_enable = False
# test_file =
# test_tags = None
# transient_age_limit = 1.0
# translate_modules = ['all']
# unaccent = False
# upgrade_path =
# websocket_keep_alive_timeout = 3600
# websocket_rate_limit_burst = 10
# websocket_rate_limit_delay = 0.2
# without_demo = False
# workers = None
# x_sendfile = False

0
server/server.log Normal file
View File

0
setup/alembic1.py Normal file
View File

12
setup/alembic2.py Normal file
View File

@ -0,0 +1,12 @@
import subprocess
def run_service():
bash_cmd = f"""
source /home/lqs1/app/venv/bin/activate && \
cd /home/lqs1/app/server/database && \
alembic revision -m "Initial migration" --autogenerate
"""
subprocess.Popen(bash_cmd, shell=True, executable='/bin/bash')
if __name__ == '__main__':
run_service()

19
setup/alembic3.py Normal file
View File

@ -0,0 +1,19 @@
import subprocess
def run_service():
# 激活虚拟环境并运行服务
# 注意这里的命令被组合成了一个bash脚本字符串
# 首先激活虚拟环境然后改变目录最后执行alembic命令
bash_cmd = f"""
source /home/lqs1/app/venv/bin/activate && \
cd /home/lqs1/app/server/database && \
alembic upgrade head
"""
# 使用 Popen 执行bash命令
# 注意这里使用shell=True因为我们正在执行一个bash脚本
# executable='/bin/bash' 是可选的因为默认就是bash但明确指出也无妨
subprocess.Popen(bash_cmd, shell=True, executable='/bin/bash')
if __name__ == '__main__':
run_service()

17
setup/start.py Normal file
View File

@ -0,0 +1,17 @@
# start_service.py
import subprocess
def run_service():
# 激活虚拟环境
activate_cmd = '/home/lqs1/桌面/venv/bin/activate'
# 运行服务
service_cmd = ['uvicorn', 'server.main:ST', '--reload']
# 构建完整的命令
full_cmd = f'source {activate_cmd} && cd /home/lqs1/桌面/ST/ && uvicorn server.main:app --reload'
# 使用 Popen 执行命令
subprocess.Popen(full_cmd, shell=True, executable='/bin/bash')
if __name__ == '__main__':
run_service()

16
setup/stop.py Normal file
View File

@ -0,0 +1,16 @@
# stop_service.py
import subprocess
def stop_service():
# 查找 uvicorn 进程 ID
find_cmd = "ps aux | grep 'uvicorn server.main:app --reload' | grep -v grep | awk '{print $2}'"
process = subprocess.run(find_cmd, shell=True, capture_output=True, text=True)
pid = process.stdout.strip()
if pid:
# 终止进程
kill_cmd = f"kill {pid}"
subprocess.run(kill_cmd, shell=True)
if __name__ == '__main__':
stop_service()

30
vue/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# 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

3
vue/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

33
vue/README.md Normal file
View File

@ -0,0 +1,33 @@
# vue
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://vitejs.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
```

BIN
vue/command.xmind Normal file

Binary file not shown.

1
vue/env.d.ts vendored Normal file
View File

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

23
vue/index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<!-- <style>
body{
width: 100%;
height: 100%;
background-color: rgb(240, 230, 140) ;
/* 渐变背景色 */
background: radial-gradient(circle,rgb(240, 230, 140),rgb(230, 220, 125),
rgb(225, 205, 60),rgb(200, 180, 15));
}
</style> -->

1901
vue/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
vue/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "vue",
"version": "0.0.0",
"private": false,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"axios": "^1.7.5",
"element-plus": "^2.8.1",
"pinia": "^2.1.7",
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.14.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^6.2.0",
"typescript": "~5.4.0",
"vite": "^5.3.1",
"vue-tsc": "^2.0.21"
}
}

BIN
vue/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

0
vue/src/.env Normal file
View File

41
vue/src/App.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<header>
<!-- 显示导航组件的条件 -->
<HomeView v-if="isLoggedIn" />
<RouterLink :to="{ name: '登陆' }" v-if="!isLoggedIn"></RouterLink>
</header>
<div class="wrapper">
<RouterView></RouterView>
</div>
</template>
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HomeView from '@/components/Home.vue'
import { useRouter, useRoute } from 'vue-router';
import { onMounted, ref } from 'vue';
const router = useRouter();
//
const isLoggedIn = ref(false);
//
const checkLoginStatus = () => { isLoggedIn.value = localStorage.getItem('isLoggedIn') === 'true'; };
onMounted(() => { checkLoginStatus(); });
//
router.afterEach(() => { checkLoginStatus(); });
</script>
<!-- <style scoped>
/* rgb(240, 230, 140); */
header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 5vh;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 1000;
}
</style> -->

BIN
vue/src/assets/6shu.mp4 Normal file

Binary file not shown.

86
vue/src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-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;
font-weight: normal;
}
body {
min-height: 100vh;
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;
}

BIN
vue/src/assets/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

1
vue/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

35
vue/src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

409
vue/src/chishi/login灯.vue Normal file
View File

@ -0,0 +1,409 @@
<template>
<div class="cube">
<div class="top">
<h2></h2>
</div>
<div class="box">
<span style="--i:0">
<h2></h2>
<h2></h2>
</span>
<span style="--i:1">
<h2></h2>
<h2></h2>
</span>
<span style="--i:2">
<h2></h2>
<h2></h2>
</span>
<span style="--i:3">
<h2></h2>
<h2></h2>
</span>
</div>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
/* 解决手机浏览器点击有选框的问题 */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
min-height: 100vh;
background-color: #050505;
}
.cube {
width: 300px;
height: 300px;
position: relative;
transform-style: preserve-3d;
animation: animate 10s linear infinite;
}
@keyframes animate {
0% {
transform: rotateX(-30deg) rotateY(0deg);
}
100% {
transform: rotateX(-30deg) rotateY(-360deg);
}
}
.cube div {
width: 100%;
height: 100%;
position: absolute;
transform-style: preserve-3d;
}
.cube .box span {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: linear-gradient(#151515, rgb(255, 40, 40));
position: absolute;
transform: rotateY(calc(90deg * var(--i))) translateZ(150px);
transform-style: preserve-3d;
}
.cube .box span h2 {
font-size: 10em;
color: #ffffff;
position: absolute;
transform: translateZ(50px);
}
.cube .box span h2:nth-child(2) {
color: rgba(0, 0, 0, 0.1);
filter: blur(2px);
transform: translateZ(0) translateY(20px);
}
.cube .top {
display: flex;
justify-content: center;
align-items: center;
background-color: #151515;
position: absolute;
transform: rotateX(90deg) translateZ(150px);
}
.cube .top::before {
content: "";
width: 100%;
height: 100%;
background-color: rgb(255, 50, 50);
filter: blur(20px);
box-shadow: 0 0 120px rgba(255, 50, 50, 0.2), 0 0 200px rgba(255, 50, 50, 0.4),
0 0 300px rgba(255, 50, 50, 0.6), 0 0 400px rgba(255, 50, 50, 0.8),
0 0 500px rgba(255, 50, 50, 1);
position: absolute;
transform: translateZ(-400px);
}
.cube .top h2 {
font-size: 10em;
color: #fff;
position: absolute;
transform: translateZ(70px) rotateX(-90deg) rotateY(45deg);
}
* {
/* 常规初始化 */
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "阿里巴巴普惠体";
/* 解决手机浏览器点击有选框的问题 */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
/* 常规居中显示 */
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
min-height: 100vh;
background-color: #3f3f3f;
}
.box {
/* flex 让盒子横着排列 */
display: flex;
/* 元素之间的距离 */
gap: 10px;
/* 3D模式 */
transform-style: preserve-3d;
/* 旋转一下,看着立体一点 */
transform: rotateY(30deg) rotateX(10deg);
}
.box .text {
/* 单个元素的宽高 */
width: 100px;
height: 100px;
/* 给个定位 */
position: relative;
/* 3D模式 */
transform-style: preserve-3d;
/* 过度动画时间、动画曲线 */
transition: 2.5s ease-in-out;
/* 动画延时,通过 calc 和 --i 计算出每个 text 元素对应的延时时间 */
transition-delay: calc(0.25s * var(--i));
}
/* 方块左边空的一面 */
.box .text::before {
content: "";
width: 100%;
height: 100%;
background-color: #373737;
position: absolute;
/* 下面的旋转位移都按左边为基础 */
transform-origin: left;
/* 调整位置盖住左边面 */
transform: rotateY(90deg) translateX(-50%);
}
.box .text:first-child:before {
/* 第一块给个不一样的颜色 */
background-color: #e95e87;
}
.box:hover .text {
/* 鼠标放上去旋转一定角度 */
transform: rotateX(-540deg);
}
.box:hover .text:first-child {
/* 第一块来个不同方向 */
transform: rotateX(540deg);
}
.box .text span {
/* 内部元素居中对齐 */
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
/* 背景渐变颜色 */
background: linear-gradient(#434343, #535353);
/* 字体大小、粗细、颜色 */
font-size: 4em;
font-weight: 500;
color: #fff;
position: absolute;
/* 3D模式 */
transform-style: preserve-3d;
/* 旋转 90deg 的倍数,组成正方体四个面,调整到合适位置 */
transform: rotateX(calc(90deg * var(--j))) translateZ(50px);
}
.box .text:first-child span {
/* 第一个颜色不一样 */
background: linear-gradient(#e97089, #ed82ad);
}
/* 立体文字*/
* {
/* 常规初始化 */
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
/* 常规居中显示 */
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
min-height: 100vh;
/* 渐变背景色 */
background: radial-gradient(circle, #0077ee, #1f4f99, #1b2949, #000);
}
.box {
/* 父盒子确定整体宽高 */
width: 50vmin;
height: 50vmin;
/* 3D模式给一个透视距离 */
transform-style: preserve-3d;
perspective: 100vmin;
position: relative;
}
.layer {
/* 内部元素居中对齐,不要也行,就是有些时候不在中间 */
display: flex;
justify-content: center;
align-items: center;
/* 文字区域的宽高 */
width: 100%;
height: 100%;
/* 3D模式 */
transform-style: preserve-3d;
/* 先旋转一下方便查看样式,也是配合动画的初始化 */
transform: rotateX(40deg) rotateY(40deg) translateZ(0);
/* 旋转动画,只是为了更好地展示文字立体效果 */
animation: animate 10s infinite alternate ease-in-out;
/* 定位叠起来 */
position: absolute;
}
@keyframes animate {
100% {
/* 动画就是简单左右旋转 */
transform: rotateX(-40deg) rotateY(-40deg);
}
}
.layer::after {
/* 给每层的 after 填充文字 */
content: "立体文字";
/* 文字颜色、大小、粗细、行高、居中 */
color: #f5f5f5;
font-size: 20vmin;
font-weight: bold;
line-height: 22vmin;
text-align: center;
/* 文字阴影 */
text-shadow: 0.4vmin 0 1vmin rgba(0, 0, 0, 0.2);
position: absolute;
}
.layer:nth-child(1)::after {
/* 第一层就是正常显示的一层 */
transform: translateZ(0);
}
.layer:nth-child(2)::after {
/* 后面的每一层都逐层 -0.15vmin */
transform: translateZ(-0.15vmin);
}
.layer:nth-child(3)::after {
transform: translateZ(-0.3vmin);
}
.layer:nth-child(4)::after {
transform: translateZ(-0.45vmin);
}
.layer:nth-child(5)::after {
transform: translateZ(-0.6vmin);
}
.layer:nth-child(6)::after {
transform: translateZ(-0.75vmin);
}
.layer:nth-child(7)::after {
transform: translateZ(-0.9vmin);
}
.layer:nth-child(8)::after {
transform: translateZ(-1.05vmin);
}
.layer:nth-child(9)::after {
transform: translateZ(-1.2vmin);
}
.layer:nth-child(10)::after {
transform: translateZ(-1.35vmin);
}
.layer:nth-child(11)::after {
transform: translateZ(-1.5vmin);
}
.layer:nth-child(12)::after {
transform: translateZ(-1.65vmin);
}
.layer:nth-child(13)::after {
transform: translateZ(-1.8vmin);
}
.layer:nth-child(14)::after {
transform: translateZ(-1.95vmin);
}
.layer:nth-child(15)::after {
transform: translateZ(-2.1vmin);
}
.layer:nth-child(16)::after {
transform: translateZ(-2.25vmin);
}
.layer:nth-child(17)::after {
transform: translateZ(-2.4vmin);
}
.layer:nth-child(18)::after {
transform: translateZ(-2.55vmin);
}
.layer:nth-child(19)::after {
transform: translateZ(-2.7vmin);
}
.layer:nth-child(20)::after {
transform: translateZ(-2.85vmin);
}
/* 第 10 层往后 */
.layer:nth-child(n + 10)::after {
/* 文本宽度和颜色,后面会覆盖掉,其实也就中间有一个阴影层次感 */
-webkit-text-stroke: 0.3vmin rgba(0, 0, 0, 0.2);
}
/* 第 11 层往后 */
.layer:nth-child(n + 11)::after {
/* 文字要大一圈,换个颜色 */
-webkit-text-stroke: 1.5vmin #1f90ff;
/* 字体阴影,还是加强层次感 */
text-shadow: 0.6vmin 0 0.6vmin #00366b, 0.5vmin 0.5vmin 0.5vmin #002951,
0 0.6vmin 0.6vmin #00366b;
}
/* 第 12 层往后 */
.layer:nth-child(n + 12)::after {
/* 换个字体颜色 */
-webkit-text-stroke: 1.5vmin #0077ee;
}
.layer:last-child:after {
/* 最后一层来个黑色阴影垫底 */
-webkit-text-stroke: 1.5vmin rgba(0, 0, 0, 0.1);
}
.layer:first-child:after {
/* 把第一层的颜色改成最亮,字体阴影也不要 */
color: #fff;
text-shadow: none;
}
</style>

View File

View File

View File

@ -0,0 +1,433 @@
<template>
<div class="cube">
<div class="top">
<h2></h2>
</div>
<div class="box">
<span style="--i:0">
<h2></h2>
<h2></h2>
</span>
<span style="--i:1">
<h2></h2>
<h2></h2>
</span>
<span style="--i:2">
<h2></h2>
<h2></h2>
</span>
<span style="--i:3">
<h2></h2>
<h2></h2>
</span>
</div>
</div>
<div class="box">
<div class="text" style="--i:0">
<span style="--j:0">3D</span>
<span style="--j:1">3</span>
<span style="--j:2">2</span>
<span style="--j:3">6</span>
</div>
<div class="text" style="--i:1">
<span style="--j:0"></span>
<span style="--j:1">1</span>
<span style="--j:2">0</span>
<span style="--j:3">8</span>
</div>
<div class="text" style="--i:2">
<span style="--j:0"></span>
<span style="--j:1">3</span>
<span style="--j:2">2</span>
<span style="--j:3">5</span>
</div>
<div class="text" style="--i:3">
<span style="--j:0"></span>
<span style="--j:1">1</span>
<span style="--j:2">4</span>
<span style="--j:3">7</span>
</div>
</div>
<div class="box">
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
<div class="layer"></div>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
/* 解决手机浏览器点击有选框的问题 */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.cube {
width: 300px;
height: 300px;
position: relative;
transform-style: preserve-3d;
animation: animate 10s linear infinite;
}
@keyframes animate {
0% {
transform: rotateX(-30deg) rotateY(0deg);
}
100% {
transform: rotateX(-30deg) rotateY(-360deg);
}
}
.cube div {
width: 100%;
height: 100%;
position: absolute;
transform-style: preserve-3d;
}
.cube .box span {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: linear-gradient(#151515, rgb(255, 40, 40));
position: absolute;
transform: rotateY(calc(90deg * var(--i))) translateZ(150px);
transform-style: preserve-3d;
}
.cube .box span h2 {
font-size: 10em;
color: #ffffff;
position: absolute;
transform: translateZ(50px);
}
.cube .box span h2:nth-child(2) {
color: rgba(0, 0, 0, 0.1);
filter: blur(2px);
transform: translateZ(0) translateY(20px);
}
.cube .top {
display: flex;
justify-content: center;
align-items: center;
background-color: #151515;
position: absolute;
transform: rotateX(90deg) translateZ(150px);
}
.cube .top::before {
content: "";
width: 100%;
height: 100%;
background-color: rgb(255, 50, 50);
filter: blur(20px);
box-shadow: 0 0 120px rgba(255, 50, 50, 0.2), 0 0 200px rgba(255, 50, 50, 0.4),
0 0 300px rgba(255, 50, 50, 0.6), 0 0 400px rgba(255, 50, 50, 0.8),
0 0 500px rgba(255, 50, 50, 1);
position: absolute;
transform: translateZ(-400px);
}
.cube .top h2 {
font-size: 10em;
color: #fff;
position: absolute;
transform: translateZ(70px) rotateX(-90deg) rotateY(45deg);
}
* {
/* 常规初始化 */
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "阿里巴巴普惠体";
/* 解决手机浏览器点击有选框的问题 */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.box {
/* flex 让盒子横着排列 */
display: flex;
/* 元素之间的距离 */
gap: 10px;
/* 3D模式 */
transform-style: preserve-3d;
/* 旋转一下,看着立体一点 */
transform: rotateY(30deg) rotateX(10deg);
}
.box .text {
/* 单个元素的宽高 */
width: 100px;
height: 100px;
/* 给个定位 */
position: relative;
/* 3D模式 */
transform-style: preserve-3d;
/* 过度动画时间、动画曲线 */
transition: 2.5s ease-in-out;
/* 动画延时,通过 calc 和 --i 计算出每个 text 元素对应的延时时间 */
transition-delay: calc(0.25s * var(--i));
}
/* 方块左边空的一面 */
.box .text::before {
content: "";
width: 100%;
height: 100%;
background-color: #373737;
position: absolute;
/* 下面的旋转位移都按左边为基础 */
transform-origin: left;
/* 调整位置盖住左边面 */
transform: rotateY(90deg) translateX(-50%);
}
.box .text:first-child:before {
/* 第一块给个不一样的颜色 */
background-color: #e95e87;
}
.box:hover .text {
/* 鼠标放上去旋转一定角度 */
transform: rotateX(-540deg);
}
.box:hover .text:first-child {
/* 第一块来个不同方向 */
transform: rotateX(540deg);
}
.box .text span {
/* 内部元素居中对齐 */
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
/* 背景渐变颜色 */
background: linear-gradient(#434343, #535353);
/* 字体大小、粗细、颜色 */
font-size: 4em;
font-weight: 500;
color: #fff;
position: absolute;
/* 3D模式 */
transform-style: preserve-3d;
/* 旋转 90deg 的倍数,组成正方体四个面,调整到合适位置 */
transform: rotateX(calc(90deg * var(--j))) translateZ(50px);
}
.box .text:first-child span {
/* 第一个颜色不一样 */
background: linear-gradient(#e97089, #ed82ad);
}
/* 立体文字*/
* {
/* 常规初始化 */
margin: 0;
padding: 0;
box-sizing: border-box;
}
.box {
/* 父盒子确定整体宽高 */
width: 50vmin;
height: 50vmin;
/* 3D模式给一个透视距离 */
transform-style: preserve-3d;
perspective: 100vmin;
position: relative;
}
.layer {
/* 内部元素居中对齐,不要也行,就是有些时候不在中间 */
display: flex;
justify-content: center;
align-items: center;
/* 文字区域的宽高 */
width: 100%;
height: 100%;
/* 3D模式 */
transform-style: preserve-3d;
/* 先旋转一下方便查看样式,也是配合动画的初始化 */
transform: rotateX(40deg) rotateY(40deg) translateZ(0);
/* 旋转动画,只是为了更好地展示文字立体效果 */
animation: animate 10s infinite alternate ease-in-out;
/* 定位叠起来 */
position: absolute;
}
@keyframes animate {
100% {
/* 动画就是简单左右旋转 */
transform: rotateX(-40deg) rotateY(-40deg);
}
}
.layer::after {
/* 给每层的 after 填充文字 */
content: "立体文字";
/* 文字颜色、大小、粗细、行高、居中 */
color: #f5f5f5;
font-size: 20vmin;
font-weight: bold;
line-height: 22vmin;
text-align: center;
/* 文字阴影 */
text-shadow: 0.4vmin 0 1vmin rgba(0, 0, 0, 0.2);
position: absolute;
}
.layer:nth-child(1)::after {
/* 第一层就是正常显示的一层 */
transform: translateZ(0);
}
.layer:nth-child(2)::after {
/* 后面的每一层都逐层 -0.15vmin */
transform: translateZ(-0.15vmin);
}
.layer:nth-child(3)::after {
transform: translateZ(-0.3vmin);
}
.layer:nth-child(4)::after {
transform: translateZ(-0.45vmin);
}
.layer:nth-child(5)::after {
transform: translateZ(-0.6vmin);
}
.layer:nth-child(6)::after {
transform: translateZ(-0.75vmin);
}
.layer:nth-child(7)::after {
transform: translateZ(-0.9vmin);
}
.layer:nth-child(8)::after {
transform: translateZ(-1.05vmin);
}
.layer:nth-child(9)::after {
transform: translateZ(-1.2vmin);
}
.layer:nth-child(10)::after {
transform: translateZ(-1.35vmin);
}
.layer:nth-child(11)::after {
transform: translateZ(-1.5vmin);
}
.layer:nth-child(12)::after {
transform: translateZ(-1.65vmin);
}
.layer:nth-child(13)::after {
transform: translateZ(-1.8vmin);
}
.layer:nth-child(14)::after {
transform: translateZ(-1.95vmin);
}
.layer:nth-child(15)::after {
transform: translateZ(-2.1vmin);
}
.layer:nth-child(16)::after {
transform: translateZ(-2.25vmin);
}
.layer:nth-child(17)::after {
transform: translateZ(-2.4vmin);
}
.layer:nth-child(18)::after {
transform: translateZ(-2.55vmin);
}
.layer:nth-child(19)::after {
transform: translateZ(-2.7vmin);
}
.layer:nth-child(20)::after {
transform: translateZ(-2.85vmin);
}
/* 第 10 层往后 */
.layer:nth-child(n + 10)::after {
/* 文本宽度和颜色,后面会覆盖掉,其实也就中间有一个阴影层次感 */
-webkit-text-stroke: 0.3vmin rgba(0, 0, 0, 0.2);
}
/* 第 11 层往后 */
.layer:nth-child(n + 11)::after {
/* 文字要大一圈,换个颜色 */
-webkit-text-stroke: 1.5vmin #1f90ff;
/* 字体阴影,还是加强层次感 */
text-shadow: 0.6vmin 0 0.6vmin #00366b, 0.5vmin 0.5vmin 0.5vmin #002951,
0 0.6vmin 0.6vmin #00366b;
}
/* 第 12 层往后 */
.layer:nth-child(n + 12)::after {
/* 换个字体颜色 */
-webkit-text-stroke: 1.5vmin #0077ee;
}
.layer:last-child:after {
/* 最后一层来个黑色阴影垫底 */
-webkit-text-stroke: 1.5vmin rgba(0, 0, 0, 0.1);
}
.layer:first-child:after {
/* 把第一层的颜色改成最亮,字体阴影也不要 */
color: #fff;
text-shadow: none;
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div>
<input v-model="name" placeholder="Name" />
<input v-model="username" placeholder="Username" />
<input v-model="password" placeholder="Password" />
<button @click="register">Register</button>
<p class="error-message">{{ errorMessage }}</p>
</div>
</template>
<script setup lang="ts">
import axios from 'axios';
import { ref, getCurrentInstance } from 'vue';
// setup
const { proxy } = getCurrentInstance();
const name = ref('');
const username = ref('');
const password = ref('');
const errorMessage = ref('');
const register = async () => {
try {
const response = await axios.post(`${proxy.$baseUrl}/admin/create`, {
name: name.value,
username: username.value,
password: password.value
});
console.log(response.data);
errorMessage.value = ''; //
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
errorMessage.value = error.response.data.message || '注册失败,请检查输入信息。';
} else {
errorMessage.value = '网络错误,请稍后再试。';
}
} else {
errorMessage.value = '未知错误,请稍后再试。';
}
console.error(error);
}
};
</script>
<style scoped>
.error-message {
color: red;
}
</style>

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://vitejs.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,49 @@
<template>
<div>
<select @change="navigateToRoute">
<option v-for="route in routes" :key="route.path" :value="route.path">
{{ route.name }}
</option>
</select>
</div>
<div>
<button @click="logout">注销{{ currentUser?.username }}</button>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/userStore';
const userStore = useUserStore();
const router = useRouter();
const currentUser = userStore.currentUser;
const routes = [
{ path: '/', name: '登录' },
{ path: '/home', name: '首页' },
{ path: '/list', name: '列表' },
{ path: '/form', name: '表单' },
{ path: '/kanban', name: '看板' },
{ path: '/ceshi', name: '测试' },
];
function navigateToRoute(event) {
const routePath = event.target.value;
router.push(routePath);
}
const logout = async () => {
// token
localStorage.removeItem('accessToken');
//
// router.push({ name: '' });
window.location.href = '/'; // /login
localStorage.setItem('isLoggedIn', 'False'); //
console.log('执行完毕');
};
</script>

View File

@ -0,0 +1,10 @@
<template>
<div>
<input/>
</div>
</template>
<script>
</script>

View File

@ -0,0 +1,42 @@
<template>
<div>
<button @click="fetchAdmins">获取管理员列表</button>
<ul>
<li v-for="admin in admins" :key="admin.id">
{{ admin.name }} - {{ admin.username }}
</li>
</ul>
<p>{{ errorMessage }}</p>
</div>
</template>
<script setup lang="ts">
import axios from 'axios';
import { ref, getCurrentInstance } from 'vue';
// setup
const { proxy } = getCurrentInstance();
const admins = ref([]);
const errorMessage = ref('');
const fetchAdmins = async () => {
try {
const response = await axios.get(`${proxy.$baseUrl}/admin/search`);
admins.value = response.data;
errorMessage.value = '';
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
errorMessage.value = error.response.data.detail || '获取管理员列表失败,请检查输入信息。';
} else {
errorMessage.value = '网络错误,请稍后再试。';
}
} else {
errorMessage.value = '未知错误,请稍后再试。';
}
console.error(error);
}
};
</script>

View File

@ -0,0 +1,88 @@
<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'
</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://vitejs.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/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
<br />
More instructions are available in <code>README.md</code>.
</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 subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter 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,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>

View File

19
vue/src/main.ts Normal file
View File

@ -0,0 +1,19 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import setupAxios from './setupAxios' // 引入新的 axios 配置文件
const app = createApp(App)
const baseUrl = 'http://8.134.237.70:7003';
const pinia = createPinia();
app.use(createPinia())
app.use(router)
app.config.globalProperties.$baseUrl = baseUrl;
app.config.globalProperties.$getFullUrl = (params) => {return baseUrl + params};
app.mount('#app')
setupAxios(app);

78
vue/src/router/index.ts Normal file
View File

@ -0,0 +1,78 @@
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/components/Home.vue';
import ListView from '@/components/List.vue';
import LoginView from '@/views/Login.vue';
import FormView from '@/components/Form.vue';
import KanbanView from '@/components/Kanban.vue';
import WebView from '@/views/Web.vue';
import CeShiView from '@/components/CeShi.vue';
import DatabaseManagerView from '@/views/DatabaseManager.vue';
const router = createRouter({
history: createWebHistory(
import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: '欢迎',
component: WebView,
meta: { noNav: true } // 添加元信息
},
{
path: '/web/login',
name: '登陆',
component: LoginView,
meta: { noNav: true } // 添加元信息
},
{
path: '/web/database/manager',
name: '数据库管理',
component: DatabaseManagerView,
meta: { noNav: true } // 添加元信息
},
{
path: '/home',
name: '首页',
component: HomeView
},
{
path: '/list',
name: '列表',
component: ListView
},
{
path: '/kanban',
name: '看板',
component: KanbanView
},
{
path: '/form',
name: '表单',
component: FormView
},
{
path: '/ceshi',
name: '测试',
component: CeShiView
},
]
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查是否需要导航组件
if (to.meta.noNav) {
// 不需要导航组件,直接进入
next();
} else {
// 需要导航组件,检查用户是否已登录
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; // 获取登录状态
if (!isLoggedIn && to.name !== '登陆') {
// 用户未登录,且不是前往登录页面,重定向到登录页面
next({ name: '登陆' });
} else {
// 用户已登录,或者正在前往登录页面,继续导航
next();
}
}
});
export default router;

27
vue/src/setupAxios.ts Normal file
View File

@ -0,0 +1,27 @@
import axios from 'axios';
import router from './router'; // 确保 router 已经被正确导入
// 添加请求拦截器,在这里添加 token 到请求头
axios.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; }
return config;
}, function (error) {
return Promise.reject(error);
});
// 添加响应拦截器,处理 token 过期的情况,清除本地存储中的 token 并重定向到登录页面
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
if (error.response.status === 401 && error.response.data.detail === "Token has expired") {
localStorage.removeItem('accessToken');
router.push('/web/login');
}
return Promise.reject(error);
});
// 导出一个函数,用于设置 axios 配置
export default function setupAxios(app) {
// 你可以在这里对 app 进行一些操作,比如设置全局属性等
}

12
vue/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 }
})

View File

@ -0,0 +1,26 @@
// stores/userStore.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore({
id: 'user',
state: () => ({ currentUser: null as any, }),
actions: {
async login(credentials: { database: string; username: string; password: string; accessToken: string }) {
try {
// 存储用户信息
this.currentUser = {
Database: credentials.database,
username: credentials.username,
accessToken: credentials.accessToken,
};
} catch (error) {
throw error;
}
},
logout() { this.currentUser = null; },
restoreUser() {
// 如果需要从本地存储恢复用户信息,可以在这里实现
},
},
getters: { currentUserDetails: (state) => state.currentUser, },
});

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>

View File

@ -0,0 +1,143 @@
<template>
<form @submit.prevent="handleSubmit" class="login">
<h1>create database</h1>
<div>
<label for="masterPassword">Master Password:</label>
<input type="password" id="masterPassword" v-model="masterPassword" required />
</div>
<div>
<label for="databaseName">Database Name:</label>
<input type="text" id="databaseName" v-model="databaseName" required />
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" v-model="email" required />
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" v-model="password" required />
</div>
<div>
<label for="phoneNumber">Phone number:</label>
<input type="tel" id="phoneNumber" v-model="phoneNumber" required />
</div>
<div>
<label for="language">Language:</label>
<select id="language" v-model="selectedLanguage">
<option v-for="(lang, index) in languages" :key="index" :value="lang.value">{{ lang.label }}</option>
</select>
</div>
<div>
<label for="country">Country:</label>
<select id="country" v-model="selectedCountry">
<option v-for="(country, index) in countries" :key="index" :value="country.value">{{ country.label }}
</option>
</select>
</div>
<div>
<label for="demoData">Demo data:</label>
<select id="demoData" v-model="selectedDemoData">
<option v-for="(data, index) in demoData" :key="index" :value="data.value">{{ data.label }}</option>
</select>
</div>
<button class="btn" type="submit">Submit</button>
</form>
<h1>database list</h1>
<ul>
<li v-for="(database, index) in databases" :key="index">
{{ database }}
</li>
</ul>
</template>
<script setup>
import { ref, getCurrentInstance, onMounted } from 'vue';
// import axios from 'axios';
const databases = ref([]);
onMounted(async () => {
try {
const response = await axios.get(`${proxy.$baseUrl}/`);
databases.value = response.data;
} catch (error) {
console.error('Failed to fetch databases:', error);
}
});
//
const languages = [
{ value: 'en-US', label: 'English (US)' },
{ value: 'zh-CN', label: 'Chinese (Simplified)' },
{ value: 'es-ES', label: 'Spanish (Spain)' }
];
const countries = [
{ value: 'us', label: 'United States' },
{ value: 'cn', label: 'China' },
{ value: 'es', label: 'Spain' }
];
const demoData = [
{ value: 'yes', label: 'Yes' },
{ value: 'no', label: 'No' }
];
import axios from 'axios';
// setup
const { proxy } = getCurrentInstance();
const masterPassword = ref('');
const databaseName = ref('');
const email = ref('');
const password = ref('');
const phoneNumber = ref('');
const selectedLanguage = ref('en-US');
const selectedCountry = ref('us');
const selectedDemoData = ref('no');
const handleSubmit = async () => {
console.log('Master Password:', masterPassword.value);
console.log('Database Name:', databaseName.value);
console.log('Email:', email.value);
console.log('Password:', password.value);
console.log('Phone number:', phoneNumber.value);
console.log('Language:', selectedLanguage.value);
console.log('Country:', selectedCountry.value);
console.log('Demo data:', selectedDemoData.value);
try {
const response = await axios.post(`${proxy.$baseUrl}/web/database/manager/create`, {
masterPassword: masterPassword.value,
databaseName: databaseName.value,
email: email.value,
password: password.value,
phoneNumber: phoneNumber.value,
selectedLanguage: selectedLanguage.value,
selectedCountry: selectedCountry.value,
selectedDemoData: selectedDemoData.value,
});
console.log(response.data);
errorMessage.value = ''; //
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
errorMessage.value = error.response.data.message || '创建失败,请检查输入信息。';
} else {
errorMessage.value = '网络错误,请稍后再试。';
}
} else {
errorMessage.value = '未知错误,请稍后再试。';
}
console.error(error);
}
}
</script>
<style>
/* 你可以在这里添加 CSS 样式 */
</style>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>

175
vue/src/views/Login.vue Normal file
View File

@ -0,0 +1,175 @@
<template>
<div class="fixed">
<div class="form-demo">
<div class="turn">
<div class="over">
<form @submit.prevent="login" class="login">
<h1>landing</h1>
<select id="database" v-model="selectedDatabase">
<option v-for="(db, index) in databases" :key="index" :value="db.value">{{ db.label }}
</option>
</select>
<button type="button" @click="fetchDatabases">获取数据库</button>
<p>Selected: {{ selectedDatabase }}</p>
<input placeholder="账户" type="text" id="username" v-model="username" required />
<input placeholder="密码" type="password" id="password" v-model="password" required />
<button class="btn" type="submit">登陆</button>
</form>
<form @submit.prevent="register" class="sign">
<h1>registered</h1>
<!-- 邀请码 验证码 密码确认 -->
<input v-model="name" type="text" placeholder="用户名" />
<input v-model="username" type="text" placeholder="账户" />
<input v-model="password" type="password" placeholder="密码" />
<button type="submit" class="btn">注册</button>
</form>
<button type="button" @click="dbmanage">数据库管理</button>
</div>
</div>
</div>
<h1 class="error-message">{{ errorMessage }}</h1>
</div>
</template>
<script setup lang="ts">
import axios from 'axios';
import { ref, getCurrentInstance, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/userStore';
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
const router = useRouter();
const username = ref('');
const password = ref('');
const name = ref('');
const errorMessage = ref('');
const selectedDatabase = ref([]); //
//
const PHONE_REGEX = /^1[3-9]\d{9}$/; //
const PASSWORD_REGEX = /^(?=.*\d)(?=.*[a-zA-Z]).{8,}$/; // 8
const databases = ref([]);
async function dbmanage() {
router.push('/web/database/manager');
}
//
async function fetchDatabases() {
try {
const response = await axios.get(`${proxy.$baseUrl}/`);
// ["app"]
// [{ value: 'app', label: 'App Database' }]
databases.value = response.data.map(db => ({
value: db,
label: capitalizeFirstLetter(db)
}));
if (databases.value.length === 0) {
//
router.push('/web/database/manager');
} else if (databases.value.length === 1) {
//
selectedDatabase.value = databases.value[0].value;
}
console.log(databases.value);
} catch (error) {
console.error('Failed to fetch databases:', error);
}
}
//
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
//
onMounted(() => {
fetchDatabases();
});
onMounted(() => {
userStore.restoreUser();
});
const login = async () => {
//
if (username.value !== 'admin' && !PHONE_REGEX.test(username.value)) {
errorMessage.value = 'Invalid phone number.';
return;
}
if (!PASSWORD_REGEX.test(password.value)) {
errorMessage.value = 'Password must contain at least one digit and one letter, and be at least 8 characters long.';
return;
}
try {
const response = await axios.post(`${proxy.$baseUrl}`, {
username: username.value,
password: password.value
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
userStore.login({
// databases: selectedDatabase.value,
username: username.value,
password: password.value,
})
localStorage.setItem('accessToken', response.data.access_token);
localStorage.setItem('isLoggedIn', 'true'); //
errorMessage.value = '';
router.push({ name: '列表' }); // 使
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
errorMessage.value = error.response.data.detail || '登录失败,请检查用户名和密码。';
} else {
errorMessage.value = '网络错误,请稍后再试。';
}
} else {
errorMessage.value = '未知错误,请稍后再试。';
}
console.error(error);
}
};
//
const register = async () => {
//
if (!PHONE_REGEX.test(username.value)) {
errorMessage.value = 'Invalid phone number.';
return;
}
if (!PASSWORD_REGEX.test(password.value)) {
errorMessage.value = 'Password must contain at least one digit and one letter, and be at least 8 characters long.';
return;
}
try {
const response = await axios.post(`${proxy.$baseUrl}/admin/create`, {
name: name.value,
username: username.value,
password: password.value
});
console.log(response.data);
errorMessage.value = ''; //
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
errorMessage.value = error.response.data.message || '注册失败,请检查输入信息。';
} else {
errorMessage.value = '网络错误,请稍后再试。';
}
} else {
errorMessage.value = '未知错误,请稍后再试。';
}
console.error(error);
}
};
</script>
<style></style>

13
vue/src/views/Web.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<h1>welcome</h1>
<button @click="handleLogin">login</button>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
function handleLogin() {
router.push('/web/login');
}
</script>
<style>
</style>

11
vue/src/views/new.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

14
vue/tsconfig.app.json Normal file
View File

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

11
vue/tsconfig.json Normal file
View File

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

19
vue/tsconfig.node.json Normal file
View File

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

16
vue/vite.config.ts Normal file
View File

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