first commit
This commit is contained in:
commit
7c78a5bb35
238
README.md
Normal file
238
README.md
Normal 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
BIN
command.xmind
Normal file
Binary file not shown.
49
server/.gitignore
vendored
Normal file
49
server/.gitignore
vendored
Normal 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
BIN
server/PATH_Server.xmind
Normal file
Binary file not shown.
BIN
server/PIP_Server.xmind
Normal file
BIN
server/PIP_Server.xmind
Normal file
Binary file not shown.
1
server/README.md
Normal file
1
server/README.md
Normal file
@ -0,0 +1 @@
|
||||
uvicorn server.main:app --reload
|
BIN
server/Route_Server.xmind
Normal file
BIN
server/Route_Server.xmind
Normal file
Binary file not shown.
17
server/__init__.py
Normal file
17
server/__init__.py
Normal 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)
|
0
server/addons/__init__.py
Normal file
0
server/addons/__init__.py
Normal file
0
server/addons/base/__init__.py
Normal file
0
server/addons/base/__init__.py
Normal file
0
server/addons/base/models/__init__.py
Normal file
0
server/addons/base/models/__init__.py
Normal file
49
server/addons/base/models/configparser.py
Normal file
49
server/addons/base/models/configparser.py
Normal 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)
|
121
server/addons/base/models/database.py
Normal file
121
server/addons/base/models/database.py
Normal 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()
|
0
server/addons/base/models/res_partner.py
Normal file
0
server/addons/base/models/res_partner.py
Normal file
62
server/main.py
Normal file
62
server/main.py
Normal 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
12
server/requirements.txt
Normal 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
70
server/server.conf
Normal 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
0
server/server.log
Normal file
0
setup/alembic1.py
Normal file
0
setup/alembic1.py
Normal file
12
setup/alembic2.py
Normal file
12
setup/alembic2.py
Normal 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
19
setup/alembic3.py
Normal 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
17
setup/start.py
Normal 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
16
setup/stop.py
Normal 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
30
vue/.gitignore
vendored
Normal 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
3
vue/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
33
vue/README.md
Normal file
33
vue/README.md
Normal 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
BIN
vue/command.xmind
Normal file
Binary file not shown.
1
vue/env.d.ts
vendored
Normal file
1
vue/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
23
vue/index.html
Normal file
23
vue/index.html
Normal 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
1901
vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
vue/package.json
Normal file
30
vue/package.json
Normal 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
BIN
vue/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
0
vue/src/.env
Normal file
0
vue/src/.env
Normal file
41
vue/src/App.vue
Normal file
41
vue/src/App.vue
Normal 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
BIN
vue/src/assets/6shu.mp4
Normal file
Binary file not shown.
86
vue/src/assets/base.css
Normal file
86
vue/src/assets/base.css
Normal 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
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
1
vue/src/assets/logo.svg
Normal 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
35
vue/src/assets/main.css
Normal 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
409
vue/src/chishi/login灯.vue
Normal 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>
|
||||
|
0
vue/src/chishi/文字.vue
Normal file
0
vue/src/chishi/文字.vue
Normal file
0
vue/src/chishi/计数器.vue
Normal file
0
vue/src/chishi/计数器.vue
Normal file
433
vue/src/components/CeShi.vue
Normal file
433
vue/src/components/CeShi.vue
Normal 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>
|
51
vue/src/components/Form.vue
Normal file
51
vue/src/components/Form.vue
Normal 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>
|
41
vue/src/components/HelloWorld.vue
Normal file
41
vue/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve 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>
|
49
vue/src/components/Home.vue
Normal file
49
vue/src/components/Home.vue
Normal 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>
|
10
vue/src/components/Kanban.vue
Normal file
10
vue/src/components/Kanban.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<input/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
42
vue/src/components/List.vue
Normal file
42
vue/src/components/List.vue
Normal 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>
|
88
vue/src/components/TheWelcome.vue
Normal file
88
vue/src/components/TheWelcome.vue
Normal 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>
|
||||
|
||||
Vue’s
|
||||
<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>
|
87
vue/src/components/WelcomeItem.vue
Normal file
87
vue/src/components/WelcomeItem.vue
Normal 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>
|
7
vue/src/components/icons/IconCommunity.vue
Normal file
7
vue/src/components/icons/IconCommunity.vue
Normal 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>
|
7
vue/src/components/icons/IconDocumentation.vue
Normal file
7
vue/src/components/icons/IconDocumentation.vue
Normal 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>
|
7
vue/src/components/icons/IconEcosystem.vue
Normal file
7
vue/src/components/icons/IconEcosystem.vue
Normal 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>
|
7
vue/src/components/icons/IconSupport.vue
Normal file
7
vue/src/components/icons/IconSupport.vue
Normal 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>
|
19
vue/src/components/icons/IconTooling.vue
Normal file
19
vue/src/components/icons/IconTooling.vue
Normal 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>
|
0
vue/src/components/language.vue
Normal file
0
vue/src/components/language.vue
Normal file
19
vue/src/main.ts
Normal file
19
vue/src/main.ts
Normal 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
78
vue/src/router/index.ts
Normal 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
27
vue/src/setupAxios.ts
Normal 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
12
vue/src/stores/counter.ts
Normal 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 }
|
||||
})
|
26
vue/src/stores/userStore.ts
Normal file
26
vue/src/stores/userStore.ts
Normal 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, },
|
||||
});
|
15
vue/src/views/AboutView.vue
Normal file
15
vue/src/views/AboutView.vue
Normal 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>
|
143
vue/src/views/DatabaseManager.vue
Normal file
143
vue/src/views/DatabaseManager.vue
Normal 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>
|
9
vue/src/views/HomeView.vue
Normal file
9
vue/src/views/HomeView.vue
Normal 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
175
vue/src/views/Login.vue
Normal 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
13
vue/src/views/Web.vue
Normal 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
11
vue/src/views/new.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
14
vue/tsconfig.app.json
Normal file
14
vue/tsconfig.app.json
Normal 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
11
vue/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
vue/tsconfig.node.json
Normal file
19
vue/tsconfig.node.json
Normal 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
16
vue/vite.config.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user