重构项目

This commit is contained in:
2025-04-17 16:11:44 +08:00
parent 28a113c132
commit 25ec96492f
80 changed files with 0 additions and 5014 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

31
.gitignore vendored
View File

@@ -1,31 +0,0 @@
# 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
pnpm-lock.yaml

View File

View File

@@ -1,8 +0,0 @@
{
"watch": ["*.ts"],
"execMap": {
"ts": "ts-node"
},
"ignore": ["*.test.ts"],
"ext": "ts"
}

View File

@@ -1,31 +0,0 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "tsc && node dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/ali-oss": "^6.16.11",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.12.12",
"@types/pg": "^8.11.11",
"ali-oss": "^6.21.0",
"jsonwebtoken": "^9.0.2",
"typescript": "^5.4.5"
},
"dependencies": {
"@types/node-fetch": "^2.6.11",
"axios": "^1.7.5",
"cors": "^2.8.5",
"express": "^4.19.2",
"pg": "^8.13.3"
}
}

1257
Server/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +0,0 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import Database from '../Plugs/Database'
import MountUserAgent from "../Plugs/Middleware/MountUserAgent";
import axios from "axios";
import MountIP from "../Plugs/Middleware/MountIP";
import { BlogComment as BlogCommentType } from "@/Types/Schema"
// 提交博客评论
class BlogComment extends API {
constructor() {
super('POST', '/blogComment', MountUserAgent, MountIP);
}
public async onRequset(data: any, res: any) {
let { bloguuid, content, name, _userAgent, _ip } = data;
if (!bloguuid || bloguuid.length != 32 || typeof content != 'string' || typeof name != 'string'
|| content.trim() == '' || name.trim() == '') {
return res.json(ServerStdResponse.INVALID_PARAMS);
}
// 处理数据
content = content.trim();
name = name.trim();
_ip = (_ip as string).replace('::ffff:', '');
// 获取IPAddress
let ip_address = '未知'
try {
let ipAddressRes = await axios.get(`https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=` + _ip);
if (ipAddressRes.data && ipAddressRes.data.msg == 'success') {
ip_address = ipAddressRes.data.data.countryCN == '中国' ? ipAddressRes.data.data.provinceCN : ipAddressRes.data.data.countryCN;
}
} catch (error) {
this.logger.warn('获取IP属地失败', error);
}
let blogLikeRes = await Database.query<BlogCommentType>('INSERT INTO blog_comment (uuid, content, name, ip, ip_address, user_agent, display, created_at) VALUES ($1,$2,$3,$4,$5,$6,true,$7)', [bloguuid, content.trim(), name.trim(), _ip, ip_address, _userAgent, new Date()]);
if (!blogLikeRes) {
this.logger.error('发布博客评论时,数据库发生错误');
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json(ServerStdResponse.OK);
}
}
export default BlogComment;

View File

@@ -1,31 +0,0 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import Database from '../Plugs/Database'
import { Buffer } from 'buffer';
import axios from "axios";
import { Blog } from "@/Types/Schema";
// 点赞
class BlogLike extends API {
constructor() {
super('POST', '/blogLike');
}
private defaultAccessLevel = 6;
public async onRequset(data: any, res: any) {
let { bloguuid } = data;
if (!bloguuid || bloguuid.length != 32) {
return res.json(ServerStdResponse.INVALID_PARAMS);
}
let blogLikeRes = await Database.query<Blog>('UPDATE blog SET like_count = like_count + 1 WHERE access_level > $1 AND uuid = $2 ', [this.defaultAccessLevel, bloguuid]);
if (!blogLikeRes) {
this.logger.error('点赞博客时,数据库发生错误');
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json(ServerStdResponse.OK);
}
}
export default BlogLike;

View File

@@ -1,26 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
// 删除博客
class DelBlog extends API {
constructor() {
super('DELETE', '/console/blog', Auth);
}
public async onRequset(data: any, res: any) {
let { uuid } = data;
if (!uuid) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
let execRes = await Database.query('DELETE FROM blog WHERE uuid = $1', [uuid]);
if (!execRes) {
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK });
}
}
export default DelBlog;

View File

@@ -1,26 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
// 删除资源
class DelResource extends API {
constructor() {
super('DELETE', '/console/resource', Auth);
}
public async onRequset(data: any, res: any) {
let { uuid } = data;
if (!uuid) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
let execRes = await Database.query('DELETE FROM resource WHERE uuid = $1', [uuid]);
if (!execRes) {
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK });
}
}
export default DelResource;

View File

@@ -1,22 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
import { Blog } from "@/Types/Schema";
// 获取博客列表
class GetBlogs extends API {
constructor() {
super('GET', '/console/blogs', Auth);
}
public async onRequset(data: any, res: any) {
let resourcesRes = await Database.query<Blog>("SELECT * FROM blog ORDER BY created_at DESC");
if (!resourcesRes) {
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK, data: resourcesRes });
}
}
export default GetBlogs;

View File

@@ -1,31 +0,0 @@
import { API, RequestData } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
import jwt from "jsonwebtoken";
import config from "../../config";
// 获取登录状态
class GetLoginStatus extends API {
constructor() {
super('GET', '/console/loginStatus', Auth);
}
public async onRequset(data: RequestData, res: any) {
const { uuid } = data._jwt;
const jwtPayload = {
uuid,
loginTime: Date.now()
}
const token = jwt.sign(jwtPayload, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
return res.json({
...ServerStdResponse.OK,
data: {
token
}
});
}
}
export default GetLoginStatus;

View File

@@ -1,59 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import { STS } from 'ali-oss'
import Auth from "../../Plugs/Middleware/Auth";
import config from "../../config";
// 获取OSS Token
class GetOSSToken extends API {
constructor() {
super('GET', '/console/ossToken', Auth);
}
public async onRequset(data: any, res: any) {
// 进行OSS_Upload_STS_Token获取
let sts = new STS({
accessKeyId: config.oss.accessKeyId,
accessKeySecret: config.oss.accessKeySecret
});
let policy = {
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": [
// "oss:GetObject",
// "oss:PutObject",
// "oss:ListObject"
"oss:*"
],
"Resource": [
// `acs:oss:*:*:tone-personal/${config.oss.dir}/*`.toString()
"*"
// `acs:oss:*:tone-personal/*`.toString()
]
}
]
};
try {
let sts_res = await sts.assumeRole(config.oss.roleArn, policy, config.oss.stsExpirationSec);
let sts_token: any = {
AccessKeyId: sts_res.credentials.AccessKeyId,
AccessKeySecret: sts_res.credentials.AccessKeySecret,
SecurityToken: sts_res.credentials.SecurityToken,
OSSRegion: config.oss.region,
Bucket: config.oss.bucket,
ExpirationSec: config.oss.stsExpirationSec,
}
this.logger.info('STS AssumeRol 成功');
res.json({ ...ServerStdResponse.OK, data: sts_token });
return;
} catch (error: any) {
this.logger.error('STS AssumeRole 获取时发生错误', error.message);
res.json(ServerStdResponse.SERVER_ERROR);
return;
}
}
}
export default GetOSSToken;

View File

@@ -1,23 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
import { Resource } from "@/Types/Schema";
// 获取资源列表
class GetResources extends API {
constructor() {
super('GET', '/console/resources', Auth);
}
public async onRequset(data: any, res: any) {
// const { uuid } = data._jwt;
let resourcesRes = await Database.query<Resource>("SELECT * FROM resource ORDER BY type, recommand, created_at DESC");
if (!resourcesRes) {
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK, data: resourcesRes });
}
}
export default GetResources;

View File

@@ -1,50 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import MountUserAgent from "../../Plugs/Middleware/MountUserAgent";
import MountIP from "../../Plugs/Middleware/MountIP";
import config from "../../config";
import jwt from 'jsonwebtoken'
import crypto from 'crypto'
import { User } from "@/Types/Schema";
// 登录
class Login extends API {
constructor() {
super('POST', '/console/login', MountUserAgent, MountIP);
}
public async onRequset(data: any, res: any) {
let { username, password, _ip, _userAgent } = data;
if (!username || !password) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
// 检查用户是否存在
let userInfoRes = await Database.query<User>('SELECT * FROM "user" WHERE username = $1', [username]);
if (!userInfoRes) {
return res.json(ServerStdResponse.SERVER_ERROR);
}
if (userInfoRes.length != 1) {
return res.json(ServerStdResponse.USER.NOTFOUND);
}
const UserInfo = userInfoRes[0];
// 检查密码是否正确
if (crypto.createHash('sha256').update(`${UserInfo.salt}${password}`).digest('hex') != UserInfo.password) {
return res.json(ServerStdResponse.USER.PASSWORD_ERROR);
}
// 准备jwtToken
const jwtPayload = {
uuid: UserInfo.uuid,
loginTime: Date.now()
}
let jwtToken = jwt.sign(jwtPayload, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
// 写入登录日志
Database.query('INSERT INTO user_login_log (user_uuid, ip, user_agent, time) VALUES ($1,$2,$3,$4)', [UserInfo.uuid, _ip, _userAgent, Date.now()]);
return res.json({ ...ServerStdResponse.OK, data: { token: jwtToken } });
}
}
export default Login;

View File

@@ -1,36 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
import crypto from 'crypto'
import { Blog } from "@/Types/Schema";
// 保存博客
class SaveBlog extends API {
constructor() {
super('POST', '/console/saveBlog', Auth);
}
public async onRequset(data: any, res: any) {
let { uuid, title, description, created_at, src, access_level } = data;
if (!title || !description || !created_at || !src || !access_level) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
let execRes: any;
if (uuid) {
// 保存
execRes = await Database.query<Blog>('UPDATE blog SET title = $1, description = $2, created_at = $3, src = $4, access_level = $5 WHERE uuid = $6', [title, description, created_at, src, access_level, uuid]);
} else {
// 新建
const uuid = crypto.createHash('md5').update(`${Math.random()}${Date.now()}`).digest('hex');
execRes = await Database.query<Blog>('INSERT INTO blog (uuid, title, description, src, created_at, access_level, visit_count, like_count) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)', [uuid, title, description, src, created_at, access_level, 0, 0]);
}
if (!execRes) {
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK });
}
}
export default SaveBlog;

View File

@@ -1,36 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
import { Resource } from "@/Types/Schema";
import Crypto from 'crypto'
// 保存资源
class SaveResource extends API {
constructor() {
super('POST', '/console/saveResource', Auth);
}
public async onRequset(data: any, res: any) {
let { uuid, type, recommand, title, describe, icon_src, addition, src } = data;
if (!type || !recommand || !title || !describe || !icon_src || !addition || !src) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
let execRes: any;
if (uuid) {
// 保存
execRes = await Database.query<Resource>('UPDATE resource SET "type" = $1, "recommand" = $2, "title" = $3, "describe" = $4, "addition" = $5, "icon_src" = $6, "src" = $7 WHERE "uuid" = $8', [type, recommand, title, describe, addition, icon_src, src, uuid]);
} else {
// 新建
uuid = Crypto.createHash('md5').update(`${Math.random()}${Date.now()}`).digest('hex');
execRes = await Database.query<Resource>('INSERT INTO resource ("uuid","type", "recommand", "title", "describe", "addition", "icon_src", "src", "created_at") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)', [uuid, type, recommand, title, describe, addition, icon_src, src, new Date()]);
}
if (!execRes) {
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK });
}
}
export default SaveResource;

View File

@@ -1,25 +0,0 @@
import { API } from "../../Plugs/API/API";
import ServerStdResponse from "../../ServerStdResponse";
import Database from '../../Plugs/Database'
import Auth from "../../Plugs/Middleware/Auth";
import crypto from 'crypto'
import { Blog } from "@/Types/Schema";
// 设置博客密码
class SetBlogPasswd extends API {
constructor() {
super('POST', '/console/setBlogPasswd', Auth);
}
public async onRequset(data: any, res: any) {
let { uuid, passwd } = data;
if (!uuid || !passwd) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
const encrypt_p = crypto.createHash('sha256').update(passwd).digest('hex');
Database.query<Blog>('UPDATE blog SET encrypt_p = $1 WHERE uuid = $2', [encrypt_p, uuid]);
return res.json({ ...ServerStdResponse.OK });
}
}
export default SetBlogPasswd;

View File

@@ -1,29 +0,0 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import Database from '../Plugs/Database'
import { BlogComment } from "@/Types/Schema";
// 获取博客评论
class GetBlogComment extends API {
constructor() {
super('GET', '/blogComment');
}
private pageSize = 100;
public async onRequset(data: any, res: any) {
let { bloguuid, page = 1 } = data;
if (!bloguuid || bloguuid.length != 32 || page < 1) {
return res.json(ServerStdResponse.INVALID_PARAMS);
}
let blogCommentRes = await Database.query<BlogComment>('SELECT content, name, ip_address, created_at FROM blog_comment WHERE uuid = $1 AND display = true ORDER BY created_at DESC LIMIT $2 OFFSET $3;', [bloguuid, this.pageSize, (page - 1) * this.pageSize]);
if (!blogCommentRes) {
this.logger.error('获取博客评论时,数据库发生错误');
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK, data: blogCommentRes });
}
}
export default GetBlogComment;

View File

@@ -1,81 +0,0 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import Database from '../Plugs/Database'
import { Buffer } from 'buffer';
import axios from "axios";
import crypto from 'crypto'
import MountIP from "../Plugs/Middleware/MountIP";
import { Blog } from "@/Types/Schema";
// 获取博客内容
class GetBlogContent extends API {
constructor() {
super('GET', '/blogContent', MountIP);
}
private AccessLevelRule = {
allow: [8, 10],
encrypt_allow: [7, 9]
};
public async onRequset(data: any, res: any) {
let { bloguuid, passwd } = data;
if (!bloguuid || bloguuid.length != 32) {
return res.json(ServerStdResponse.INVALID_PARAMS);
}
let blogContentRes = await Database.query<Blog>(`SELECT * from blog WHERE access_level in (${this.AccessLevelRule.allow.join(',')}) AND uuid = $1 `, [bloguuid]);
if (!blogContentRes) {
this.logger.error('查询时数据库发生错误');
return res.json(ServerStdResponse.SERVER_ERROR);
}
if (blogContentRes.length == 0) {
// 公开范围不可见,查询允许无连接加密查看的数据
blogContentRes = await Database.query<Blog>(`SELECT * from blog WHERE access_level in (${this.AccessLevelRule.encrypt_allow.join(',')}) AND uuid = $1 `, [bloguuid]);
if (!blogContentRes) {
this.logger.error('查询时数据库发生错误');
return res.json(ServerStdResponse.SERVER_ERROR);
}
if (blogContentRes.length != 1) {
this.logger.warn('查询的博客不存在或不可见', bloguuid);
return res.json(ServerStdResponse.BLOG.NOTFOUND);
}
// 验证密码是否存在和正确
if (!passwd) {
this.logger.warn(`客户端[${data._ip}]尝试访问受限制的博客,但并未提供密码`)
return res.json(ServerStdResponse.BLOG.PROTECT_FLAG)
}
if (crypto.createHash('sha256').update(passwd).digest('hex') != blogContentRes[0].encrypt_p){
this.logger.warn(`客户端[${data._ip}]尝试访问受限制的博客,并提供了错误的密码:${passwd}`)
return res.json(ServerStdResponse.BLOG.PASSWD_ERROR)
}
this.logger.info(`客户端[${data._ip}]访问了受限制的博客`)
}
// 返回处理后的数据
try {
const markdownUrl = blogContentRes[0].src;
const response = await axios.get(markdownUrl);
const base64Content = Buffer.from(response.data, 'utf-8').toString('base64');
// 访问次数+1
Database.query('UPDATE blog SET visit_count = visit_count + 1 WHERE uuid = $1', [bloguuid]);
return res.json({
...ServerStdResponse.OK, data: {
data: base64Content,
info: {
title: blogContentRes[0].title,
description: blogContentRes[0].description,
created_at: blogContentRes[0].created_at,
visit_count: blogContentRes[0].visit_count,
like_count: blogContentRes[0].like_count
}
}
});
} catch (error) {
this.logger.error('获取博客文章内容时发生错误', error)
return res.json(ServerStdResponse.SERVER_ERROR);
}
}
}
export default GetBlogContent;

View File

@@ -1,23 +0,0 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import Database from '../Plugs/Database'
import { Blog } from "@/Types/Schema";
// 获取博客列表
class GetBlogList extends API {
constructor() {
super('GET', '/blogList');
}
private defaultAccessLevel = 9;
public async onRequset(data: any, res: any) {
let blogListRes = await Database.query<Blog>('SELECT uuid, title, description, created_at, access_level, visit_count, like_count from blog WHERE access_level >= $1 ORDER BY created_at DESC', [this.defaultAccessLevel]);
if (!blogListRes) {
this.logger.error('查询时数据库发生错误');
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK, data: blogListRes });
}
}
export default GetBlogList;

View File

@@ -1,30 +0,0 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import Database from '../Plugs/Database'
import { Resource } from "@/Types/Schema";
// 获取资源列表
class GetResourceList extends API {
constructor() {
super('GET', '/resourceList');
}
private typeList = ['resource', 'download']
public async onRequset(data: any, res: any) {
let { type } = data;
if (!type) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
if (!this.typeList.includes(type)) {
return res.json(ServerStdResponse.INVALID_PARAMS);
}
let resourceListRes = await Database.query<Resource>('SELECT * from resource WHERE type = $1 ORDER BY recommand ASC', [type]);
if (!resourceListRes) {
this.logger.error('查询时数据库发生错误');
return res.json(ServerStdResponse.SERVER_ERROR);
}
return res.json({ ...ServerStdResponse.OK, data: resourceListRes });
}
}
export default GetResourceList;

View File

@@ -1,15 +0,0 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
// 测试接口
class GetTest extends API {
constructor() {
super('GET', '/test');
}
public async onRequset(data: any, res: any) {
res.json(ServerStdResponse.OK);
}
}
export default GetTest;

View File

@@ -1,35 +0,0 @@
import { Request, Response, NextFunction } from "express";
import Logger from "../Logger";
import jwt from 'jsonwebtoken'
interface MiddlewareFunction {
(req: Request, res: Response, next: NextFunction): void;
}
interface RequestData {
[key: string | number | symbol] : any;
_jwt?: any;
_ip?: string;
}
abstract class API {
protected logger: Logger;
public middlewareFunc: Function[] = [];
/**
* @param method API方法
* @param path API路由Path
* @param func API中间件函数
*/
constructor(public method: string, public path: string, ...func: MiddlewareFunction[]) {
this.logger = new Logger('API][' + method + '][' + path);
this.middlewareFunc.push(...func);
}
// to override
public abstract onRequset(data: RequestData, res: any): void;
}
export { API };
export type { RequestData }

View File

@@ -1,53 +0,0 @@
import express, { NextFunction, Request, Response } from "express";
import cors from "cors";
import Logger from "../Logger";
import { API } from "./API";
import ServerStdResponse from "../../ServerStdResponse";
class APILoader {
private app = express();
private logger = new Logger('APILoader');
constructor(private port?: number) {
this.logger.info('API服务加载中...');
this.app.use(express.json({ limit: '50mb' }));
this.app.use(express.urlencoded({ extended: true }));
this.app.use(cors({
origin: ['http://localhost:5173', 'http://www.tonesc.cn', 'https://www.tonesc.cn', 'http://tonesc.cn', 'https://tonesc.cn'],
methods: ['GET', 'POST', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'Access-Control-Allow-Origin', ''],
}));
}
add(api: { new(): API }) {
const instance = new api();
for (let func of instance.middlewareFunc) {
this.app[instance.method.toLowerCase() as keyof express.Application](instance.path, (req: Request, res: Response, next: NextFunction) => {
func(req, res, next);
});
this.logger.info(`[${instance.method}][${instance.path}] 已启用中间件[${func.name}]`);
}
this.app[instance.method.toLowerCase() as keyof express.Application](instance.path, (req: Request, res: Response) => {
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip;
this.logger.info(`[${instance.method}][${instance.path}] 被请求[${(ip as string).replace('::ffff:', '')}]`);
const data = Object.assign({}, req.query, req.body);
instance.onRequset(data, res);
});
this.logger.info(`[${instance.method}][${instance.path}] 加载成功`);
}
start(port?: number) {
if (this.port == undefined && port == undefined)
throw new Error('未指定API端口')
this.app.use((req: Request, res: Response) => {
this.logger.info(`[${req.method}][${req.url.split('?')[0]}] 该API不存在`);
res.json(ServerStdResponse.API_NOT_FOUND)
})
this.app.listen(port || this.port, () => {
this.logger.info(`已全部加载完成API服务开放在端口${port || this.port}`);
});
}
}
export {
APILoader,
}

View File

@@ -1,78 +0,0 @@
/**
* @file Database.ts
* @version 1.0.0
* @description PostgreSQL数据库连接池
*/
import pg from 'pg';
import Logger from "./Logger";
import config from "../config";
class DatabasePool {
private pool: pg.Pool;
private logger = new Logger('Database');
constructor() {
this.pool = this.createConnectPool();
this.logger.info("数据库连接池已创建")
this.pool.on('error', (err) => {
this.logger.error(`数据库连接池发生错误:${err}`);
});
this.testConnection();
}
private createConnectPool() {
return new pg.Pool({
host: config.pg.host,
database: config.pg.database,
user: config.pg.user,
password: config.pg.password,
connectionTimeoutMillis: 5000, // 连接超时时间
max: 20, // 最大连接数
idleTimeoutMillis: 30000, // 空闲连接超时时间
allowExitOnIdle: true // 允许空闲时退出
})
}
private async testConnection() {
let res = await this.query<{ result: number }>("SELECT 1 + 1 As result");
if (!res) {
this.logger.error("数据库连接测试失败");
return;
}
if (res[0].result == 2)
return;
this.logger.error("数据库连接测试失败?");
}
/**
* 执行SQL查询
* @param sql SQL语句
* @param values 可选的查询参数列表
* @param database 可选的数据库
* @returns Promise<any | undefined> 查询结果
*/
public async query<T>(sql: string, values?: any[]): Promise<T[] | undefined> {
let connection;
try {
connection = await this.pool.connect();
const res = await connection.query(sql, values);
return res.rows;
} catch (error) {
if (!connection) {
this.logger.error(`数据库连接失败:${error}`);
} else {
this.logger.error(`数据库查询发生错误:${error}`);
}
return undefined;
} finally {
if (connection) {
connection.release();
}
}
}
}
const Database = new DatabasePool();
export default Database;

View File

@@ -1,40 +0,0 @@
class Logger {
constructor(private namespace: string) {
}
private getTime(): string {
return new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
public info(info: string, ...args: any): void {
args = args.map((arg: any) => {
if (typeof arg === 'object') {
return JSON.stringify(arg);
}
return arg;
})
console.log(`\x1b[32m[${this.getTime()}][INFO][${this.namespace}]${info[0] == '[' ? '' : ' '}${info} ` + args.join(' ') + '\x1b[0m');
}
public warn(info: string, ...args: any): void {
args = args.map((arg: any) => {
if (typeof arg === 'object') {
return JSON.stringify(arg);
}
return arg;
})
console.log(`\x1b[33m[${this.getTime()}][WARN][${this.namespace}]${info[0] == '[' ? '' : ' '}${info} ` + args.join(' ') + '\x1b[0m');
}
public error(info: string, ...args: any): void {
args = args.map((arg: any) => {
if (typeof arg === 'object') {
return JSON.stringify(arg);
}
return arg;
})
console.log(`\x1b[31m[${this.getTime()}][ERROR][${this.namespace}]${info[0] == '[' ? '' : ' '}${info} ` + args.join(' ') + '\x1b[0m');
}
}
export default Logger;

View File

@@ -1,25 +0,0 @@
import { Request, Response, NextFunction } from "express";
import config from "../../config";
import ServerStdResponse from "../../ServerStdResponse";
import Logger from "../Logger";
import jwt from 'jsonwebtoken'
const logger = new Logger("Auth");
const Auth = (req: Request, res: Response, next: NextFunction) => {
let token = req.headers.authorization;
try {
if (!token) {
throw new Error('空Token')
}
if(typeof token != 'string' || token.indexOf('Bearer ') == -1){
throw new Error('格式错误的Token')
}
req.body._jwt = jwt.verify(token.replace('Bearer ',''), config.jwt.secret);
next()
} catch (error) {
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip;
logger.info(`API[${req.method}][${req.url.split('?')[0]}] 请求鉴权不通过[${token}][${ip}] ${error}`);
res.json(ServerStdResponse.AUTH_ERROR);
}
}
export default Auth;

View File

@@ -1,11 +0,0 @@
import { Request, Response, NextFunction } from "express"
import Logger from "../Logger";
const logger = new Logger('MountIP')
let MountIP = (req: Request, res: Response, next: NextFunction) => {
req.body._ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip;
// logger.info(`[${req.method}][${req.url.split('?')[0]}] IP解析成功${req.body._ip}`);
next();
}
export default MountIP;

View File

@@ -1,11 +0,0 @@
import { Request, Response, NextFunction } from "express"
import Logger from "../Logger";
const logger = new Logger('MountUserAgent')
let MountUserAgent = (req: Request, res: Response, next: NextFunction) => {
req.body._userAgent = req.headers['user-agent'];
logger.info(`[${req.method}][${req.url.split('?')[0]}] 用户代理解析成功:${req.body._userAgent}`);
next();
}
export default MountUserAgent;

View File

@@ -1,9 +0,0 @@
import { Request, Response, NextFunction } from "express";
import Logger from "../Logger";
const logger = new Logger("Unbind");
const Unbind = (req: Request, res: Response, next: NextFunction) => {
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip;
logger.info(`API[${req.method}][${req.url.split('?')[0]}] 请求了未绑定的接口[${ip}]`);
}
export default Unbind;

View File

@@ -1,62 +0,0 @@
import Logger from "../Plugs/Logger";
import { APILoader } from "../Plugs/API/APILoader";
import config from "../config";
// 加载API
import GetTest from "../APIs/GetTest";
import GetResourceList from "../APIs/GetResourceList";
import GetBlogList from "../APIs/GetBlogList";
import GetBlogContent from "../APIs/GetBlogContent";
import BlogLike from "../APIs/BlogLike";
import BlogComment from "../APIs/BlogComment";
import GetBlogComment from "../APIs/GetBlogComment";
import Login from "../APIs/Console/Login";
import GetResources from "../APIs/Console/GetResources";
import GetBlogs from '../APIs/Console/GetBlogs'
import SetBlogPasswd from "../APIs/Console/SetBlogPasswd";
import SaveResource from '../APIs/Console/SaveResource'
import DelResource from '../APIs/Console/DelResource'
import SaveBlog from '../APIs/Console/SaveBlog'
import DelBlog from '../APIs/Console/DelBlog'
import GetOSSToken from "../APIs/Console/GetOSSToken";
import GetLoginStatus from "../APIs/Console/GetLoginStatus";
class Server {
private logger = new Logger('Server');
public static instance: Server;
private apiLoader = new APILoader();
constructor() {
Server.instance = this;
}
public async start() {
// 加载API
this.apiLoader.add(GetTest);
this.apiLoader.add(GetResourceList);
this.apiLoader.add(GetBlogList);
this.apiLoader.add(GetBlogContent);
this.apiLoader.add(BlogLike);
this.apiLoader.add(BlogComment);
this.apiLoader.add(GetBlogComment);
this.apiLoader.add(Login);
this.apiLoader.add(GetResources);
this.apiLoader.add(SaveResource);
this.apiLoader.add(DelResource);
this.apiLoader.add(GetBlogs)
this.apiLoader.add(SaveBlog);
this.apiLoader.add(SetBlogPasswd);
this.apiLoader.add(DelBlog);
this.apiLoader.add(GetOSSToken);
this.apiLoader.add(GetLoginStatus);
this.apiLoader.start(config.apiPort);
}
}
let _Server = new Server();
export {
_Server as server,
}

View File

@@ -1,66 +0,0 @@
const ServerStdResponse = {
OK: {
code: 0,
message: 'OK'
},
PARAMS_MISSING: {
code: -1,
message: 'Parameters missing'
},
INVALID_PARAMS: {
code: -2,
message: 'Invalid parameters'
},
SERVER_ERROR: {
code: -3,
message: 'Server error'
},
API_NOT_FOUND: {
code: -4,
message: 'API not found'
},
AUTH_ERROR: {
code: -5,
message: 'Authentication error'
},
BLOG: {
NOTFOUND: {
code: -4001,
message: 'Blog not found'
},
PROTECT_FLAG: {
code: -4002,
message: 'Blog is protected, need password'
},
PASSWD_ERROR: {
code: -4003,
message: 'Blog is protected, and password is not right'
}
},
CAPTCHA: {
NOTFOUND: {
code: -5000,
message: 'captcha session not found'
},
MAX_TRY_COUNT: {
code: -5001,
message: 'captcha session try max count'
},
NOTRIGHT: {
code: -5002,
message: 'captcha is not right, please try again'
}
},
USER: {
NOTFOUND: {
code: -6000,
message: 'user is not found'
},
PASSWORD_ERROR: {
code: -6001,
message: 'user password is error'
}
}
} as const;
export default ServerStdResponse;

View File

@@ -1,101 +0,0 @@
/** 博客文章 */
export type Blog = {
/** 博客uuid */
uuid: string;
/** 标题 */
title: string;
/** 描述 */
description: string;
/** 文章md链接 */
src: string;
/** 访问级别 */
access_level: number;
/** 访问数 */
visit_count: number;
/** 点赞数 */
like_count: number;
/** 文章加密密码 */
encrypt_p?: string;
/** 发布时间 */
created_at: Date;
};
/** 博客评论 */
export type BlogComment = {
/** 评论id */
id: number;
/** 文章uuid */
uuid: string;
/** 评论内容 */
content: string;
/** 昵称 */
name: string;
/** ip地址 */
ip: string;
/** ip属地 */
ip_address: string;
/** 用户代理 */
user_agent: string;
/** 是否显示 */
display: boolean;
/** 评论时间 */
time: Date;
};
/** 资源信息 */
export type Resource = {
/** 资源ID */
uuid: string;
/** 资源类型 */
type: 'download' | 'resource';
/** 推荐程度,越小越推荐 */
recommand?: number;
/** 标题 */
title: string;
/** 描述 */
describe: string;
/** 附加信息 */
addition: any;
/** 图片url */
icon_src: string;
/** 资源src */
src: string;
/** 创建时间 */
created_at: Date;
};
/** 用户信息 */
export type User = {
/** 用户uuid */
uuid: string;
/** 用户名 */
username: string;
/** 密码salt */
salt: string;
/** 密码hashed */
password: string;
/** 手机号 */
phone: string;
/** 权限列表 */
permission: any;
/** 邮箱 */
email?: string;
/** 创建时间 */
created_at: Date;
/** 更新信息时间 */
updated_at?: Date;
};
/** 用户登录日志 */
export type UserLoginLog = {
/** 登陆记录id */
id: number;
/** 用户uuid */
user_uuid: string;
/** 登陆ip */
ip: string;
/** 用户代理 */
user_agent: string;
/** 登陆时间 */
time: Date;
};

View File

@@ -1,27 +0,0 @@
const config = {
pg: {
host: 'localhost',
// host:'server.tonesc.cn',
database: 'tonecn',
user: 'tone',
password: 'Shi15023847146'// localhost
// password: '245565' // server
},
jwt: {
secret: '17e50223f4a545ec9e36ebf08e2f71bb',
expiresIn: '1d',
},
oss: {
accessKeyId: 'LTAI5t6ceNJqmMzPkjETgTzg',
accessKeySecret: 'Eak6jFN0koDQ0yOg5KdxubjMbQ00Tk',
roleArn: 'acs:ram::1283668906130262:role/ctbu-co-oss-role',
bucket: 'tone-personal',
region: 'oss-cn-chengdu',
callbackUrl: '',
dir: 'personal-web',
stsExpirationSec: 3600
},
apiPort: 23500,
} as const;
export default config;

View File

@@ -1,11 +0,0 @@
import Logger from "./Plugs/Logger";
let logger = new Logger("Server");
logger.info('服务正启动...');
import { server } from "./Server/Server";
async function main() {
server.start();
}
main().catch((err) => {
logger.error(err);
});

View File

@@ -1,115 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"@/*": ["./*"]
},
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist/", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

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

View File

@@ -1,33 +0,0 @@
# tonecn
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
```

View File

@@ -1,10 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

View File

@@ -1,50 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Agreement: typeof import('./src/components/Common/Agreement.vue')['default']
BlogComment: typeof import('./src/components/Blog/BlogComment.vue')['default']
BlogContentToolBar: typeof import('./src/components/Blog/BlogContentToolBar.vue')['default']
Blogs: typeof import('./src/components/Console/Blogs.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElText: typeof import('element-plus/es')['ElText']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
FileOnline: typeof import('./src/components/Console/FileOnline.vue')['default']
Footer: typeof import('./src/components/Common/Footer.vue')['default']
Header: typeof import('./src/components/Common/Header.vue')['default']
Resources: typeof import('./src/components/Console/Resources.vue')['default']
RotationVerification: typeof import('./src/components/Common/RotationVerification.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Utils: typeof import('./src/components/Console/Utils.vue')['default']
}
}

1
tonecn/env.d.ts vendored
View File

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

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<link rel="icon" href="./src/assets/logo.jpg">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="./src/assets/tailwind.css">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
// 检查用户的主题偏好
const checkTheme = () => {
const htmlElement = document.documentElement;
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
htmlElement.classList.add('dark');
} else {
htmlElement.classList.remove('dark');
}
};
// 监听主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', checkTheme);
// 初始检查
checkTheme();
</script>
</body>
</html>

View File

@@ -1,45 +0,0 @@
{
"name": "tonecn",
"version": "0.0.0",
"private": true,
"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": {
"@element-plus/icons-vue": "^2.3.1",
"ace-builds": "^1.36.2",
"axios": "^1.6.8",
"highlight.js": "^11.10.0",
"marked": "^14.1.0",
"marked-highlight": "^2.1.4",
"md5": "^2.3.0",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vue3-ace-editor": "^2.2.4"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/ali-oss": "^6.16.11",
"@types/md5": "^2.3.5",
"@types/node": "^20.12.5",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"ali-oss": "^6.21.0",
"autoprefixer": "^10.4.20",
"element-plus": "^2.7.3",
"npm": "^10.8.3",
"npm-run-all2": "^6.1.2",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "~5.4.0",
"unplugin-auto-import": "^0.17.6",
"unplugin-vue-components": "^0.27.0",
"vite": "^5.2.8",
"vue-tsc": "^2.0.11"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,5 +0,0 @@
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HeaderVue from '@/components/Common/Header.vue'
import FooterVue from '@/components/Common/Footer.vue'
</script>
<template>
<header>
<HeaderVue />
</header>
<main>
<RouterView />
<FooterVue />
</main>
</template>

View File

@@ -1,15 +0,0 @@
body{
margin: 0 0;
padding: 0 0;
font-family: Arial, Helvetica, sans-serif;
}
#app {
margin: 0 0;
padding: 0 0;
}
a {
text-decoration: none;
color: #000;
}

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,64 +0,0 @@
<script setup lang='ts'>
import { request, type BaseResponseData } from '@/lib/request';
import { computed, onMounted, reactive, ref, watch, watchEffect } from 'vue';
import { useRoute } from 'vue-router';
const model = defineModel();
const route = useRoute()
const bloguuid = route.params.uuid;
const blogCommentList: any[] = reactive([]);
const loadStatus = ref(0)// 0加载中 1已加载全部评论 -1加载失败
onMounted(async () => {
await loadComment();
})
const getStatusText = computed(() => {
switch (loadStatus.value) {
case 0:
return '加载中,请稍后...'
case 1:
return '已加载全部评论'
case -1:
return '加载失败,刷新后重试'
}
})
const loadComment = async () => {
try {
let commentRes: BaseResponseData = await request.get(`/blogComment?bloguuid=${bloguuid}`);
if (commentRes.code == 0) {
blogCommentList.splice(0, blogCommentList.length);
blogCommentList.push(...commentRes.data);
loadStatus.value = 1;
} else {
throw new Error(commentRes.message);
}
} catch (error) {
console.log(error)
loadStatus.value = -1;
}
}
watchEffect(() => {
if (model.value) {
model.value = false;
loadComment();
}
})
</script>
<template>
<el-divider></el-divider>
<div class="w-full">
<div
class="border-l-[4px] border-l-[#ccc] text-[18px] pl-[15px] mb-[10px] text-[#444] dark:text-[#fff] cursor-default">
评论</div>
<div class="my-[10px]" v-for="blogcomment of blogCommentList">
<div class=" text-[#555] dark:text-[#fff]">{{ blogcomment.name }}</div>
<div class="text-[12px] text-[#888] dark:text-[#aaa]">IP属地{{ blogcomment.ip_address }}</div>
<div class="text-[12px] text-[#888] dark:text-[#aaa]">{{ new Date(blogcomment.created_at).toLocaleString()
}}</div>
<div class="py-[10px] border-b border-b-[#ddd] text-[#333] dark:text-[#fff] whitespace-pre-wrap">{{
blogcomment.content }}</div>
</div>
<div class="text-[14px] text-[#666] dark:text-[#fff] my-[15px]"> {{ getStatusText }} </div>
</div>
</template>

View File

@@ -1,99 +0,0 @@
<script setup lang='ts'>
import { Star, Edit, StarFilled } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus';
import { ref, onMounted, onUnmounted } from 'vue';
import { useRoute } from 'vue-router';
import { request, type BaseResponseData } from '@/lib/request';
const route = useRoute()
const bloguuid = route.params.uuid;
const emit = defineEmits(['comment-success'])
const inputComment = ref('')
let inputCommentName = '';
const toolBarVisible = ref(true);
const isLiked = ref(false)
const isCaptchaViewShow = ref(false)
let lastScrollTop = 0;
const handleScrollMove = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (scrollTop > lastScrollTop) {
toolBarVisible.value = false;// 下滑
} else {
toolBarVisible.value = true;// 上滑
}
lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; // For Mobile or negative scrolling
}
const likeBlog = async () => {
if (isLiked.value) {
return ElMessage.success('已经点过赞啦~')
}
try {
let likeRes: BaseResponseData = await request.post('/blogLike?bloguuid=' + bloguuid)
if (likeRes.code == 0) {
isLiked.value = true;
return ElMessage.success('点赞成功~')
} else {
throw new Error(likeRes.message);
}
} catch (error) {
console.error('点赞失败', error);
return ElMessage.warning('手速太快啦~稍后再来试试吧');
}
}
onMounted(async () => {
window.addEventListener('scroll', handleScrollMove)
});
onUnmounted(async () => {
window.removeEventListener('scroll', handleScrollMove)
})
const commentHandle = () => {
if (inputComment.value == '' || inputComment.value.trim() == '') {
return ElMessage.warning('请先填写留言内容哟~')
}
ElMessageBox.prompt('请输入留言昵称(可留空)', 'Tip', {
confirmButtonText: '提交',
cancelButtonText: '取消',
}).then(({ value }) => {
inputCommentName = value ? value : '';
submitComment();
}).catch(() => {
ElMessage.info('已取消')
})
}
const submitComment = async () => {
isCaptchaViewShow.value = false;
ElMessage.info('正在提交,请稍后')
try {
let commentRes: BaseResponseData = await request.post('blogComment', {
session: localStorage.getItem('captcha-session'),
bloguuid: bloguuid,
content: inputComment.value.trim(),
name: inputCommentName.trim() == '' ? '匿名' : inputCommentName.trim()
})
if (commentRes.code == 0) {
emit('comment-success');
inputComment.value = '';
return ElMessage.success('评论成功~');
} else {
throw new Error(commentRes.message);
}
} catch (error) {
console.error('评论失败', error)
ElMessage.error('遇到了一些问题,稍后再来试试吧~')
}
}
</script>
<template>
<transition name="el-zoom-in-bottom">
<div class="fixed bottom-0 left-0 w-full bg-white dark:bg-[#111] shadow flex justify-center items-center" v-show="toolBarVisible">
<div class="w-full max-w-[600px] flex items-center justify-center py-[12px] px-[20px]">
<el-input v-model="inputComment" autosize type="textarea" class="mr-[12px]" placeholder="快来留下你的评论吧~"
clearable />
<el-button type="primary" :icon="Edit" circle @click="commentHandle"></el-button>
<el-button type="danger" :icon="isLiked ? StarFilled : Star" @click="likeBlog"
circle></el-button>
</div>
</div>
</transition>
</template>

View File

@@ -1,30 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
const isShow = ref(true)
const closeSite = () => {
window.location.href = 'about:blank';
}
</script>
<template>
<el-dialog v-model="isShow" title="网站使用协议" width="80%">
<div>欢迎使用本网站在使用本网站之前请仔细阅读以下使用协议</div>
<li>关于网站本网站是个人创建和维护的主要用于收藏各类资源和工具编写关于合法计算机技术的日记并提供与我交流的联系方式</li>
<li>
资源收藏声明本网站的个人收藏资源个人收藏工具栏目包含但不限于第三方链接这些资源仅供个人学习和参考本站对这些第三方链接及其内容不作任何形式的推广或认可链接内容观点或相关信息归原作者或所有者所有与本网站无关
</li>
<li>风险和责任访问者在使用这些第三方链接时应自行判断其内容的适用性并自行承担相关风险本网站及其所有者不承担因使用这些链接而产生的任何直接或间接损失的责任</li>
<li>内容和交流本网站的日记部分包含对合法计算机技术的个人见解和经验分享访问者可以通过提供的联系方式与我就计算机技术话题进行合法交流</li>
<li>版权和知识产权本网站的内容包括文本图像和代码除非另有声明均为本网站所有者个人创作并拥有版权未经许可不得复制分发或以其他方式使用这些内容</li>
<li>同意协议通过使用或浏览本网站您表示您已阅读理解并同意遵守本协议的条款如果您不同意本协议的任何部分<u>停止使用</u>本网站</li>
<li>变更和更新本网站的所有者保留随时更新或修改本使用协议的权利任何此类更改将在本网站上发布并生效</li>
<br />
<div>本使用协议的目的是确保网站的有效运行并保护访问者及网站所有者的合法权益感谢您的理解和支持</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeSite">我不接受</el-button>
<el-button type="primary" @click="$emit('closeAgreement')">我接受</el-button>
</div>
</template>
</el-dialog>
</template>

View File

@@ -1,84 +0,0 @@
<script setup lang="ts">
import copyText from '@/lib/copyText';
import { ElMessage } from 'element-plus'
let copyTextwithMsg = (text : string) => {
if(copyText(text)){
ElMessage.success('复制成功')
}else{
ElMessage.error('复制失败')
}
}
</script>
<template>
<div class="h-[1px] w-full bg-[#eee]"></div>
<div class="h-[100px] w-full mx-auto max-w-[1170px] flex justify-between items-center sm:flex-row flex-col" >
<div class="text-[12px] ml-0 sm:ml-[25px] cursor-default my-[22px] sm:my-0 mb-[10px] text-center sm:text-left">
<a href="https://beian.miit.gov.cn/">
<div class="mt-[5px] cursor-pointer dark:text-[#ccc]">备案号渝ICP备2023009516号-1</div>
</a>
<div class="mt-[6px] sm:mt-0 dark:text-[#ccc]">Copyright ©2020-{{ new Date().getFullYear() }} TONE All Rights Reserved.</div>
</div>
<div class="mr-0 sm:mr-[25px] flex sm:pb-0 pb-[20px]">
<el-popover trigger="click" placement="top" :width="160">
<p>QQ号3341154833</p>
<div style="text-align: center; margin: 0">
<el-button size="small" type="primary" @click="copyTextwithMsg('3341154833')">复制</el-button>
</div>
<template #reference>
<div class="mx-[8px] w-[30px] h-[30px] cursor-pointer border-[1.5px] border-[#eee] flex justify-center items-center rounded-[10px] transition-all duration-300 hover:bg-[#eee]">
<svg t="1705909811918" class="w-[20px] h-[20px]" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="5851" width="24" height="24">
<path
d="M824.8 613.2c-16-51.4-34.4-94.6-62.7-165.3C766.5 262.2 689.3 112 511.5 112 331.7 112 256.2 265.2 261 447.9c-28.4 70.8-46.7 113.7-62.7 165.3-34 109.5-23 154.8-14.6 155.8 18 2.2 70.1-82.4 70.1-82.4 0 49 25.2 112.9 79.8 159-26.4 8.1-85.7 29.9-71.6 53.8 11.4 19.3 196.2 12.3 249.5 6.3 53.3 6 238.1 13 249.5-6.3 14.1-23.8-45.3-45.7-71.6-53.8 54.6-46.2 79.8-110.1 79.8-159 0 0 52.1 84.6 70.1 82.4 8.5-1.1 19.5-46.4-14.5-155.8z"
p-id="5852" fill="#8a8a8a"></path>
</svg>
</div>
</template>
</el-popover>
<el-popover trigger="click" placement="top" :width="130">
<p>微信号tone0121</p>
<div style="text-align: center; margin: 0">
<el-button size="small" type="primary" @click="copyTextwithMsg('tone0121')">复制</el-button>
</div>
<template #reference>
<div class="mx-[8px] w-[30px] h-[30px] cursor-pointer border-[1.5px] border-[#eee] flex justify-center items-center rounded-[10px] transition-all duration-300 hover:bg-[#eee]">
<svg t="1705909888071" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="7135" width="24" height="24">
<path
d="M664.250054 368.541681c10.015098 0 19.892049 0.732687 29.67281 1.795902-26.647917-122.810047-159.358451-214.077703-310.826188-214.077703-169.353083 0-308.085774 114.232694-308.085774 259.274068 0 83.708494 46.165436 152.460344 123.281791 205.78483l-30.80868 91.730191 107.688651-53.455469c38.558178 7.53665 69.459978 15.308661 107.924012 15.308661 9.66308 0 19.230993-0.470721 28.752858-1.225921-6.025227-20.36584-9.521864-41.723264-9.521864-63.862493C402.328693 476.632491 517.908058 368.541681 664.250054 368.541681zM498.62897 285.87389c23.200398 0 38.557154 15.120372 38.557154 38.061874 0 22.846334-15.356756 38.156018-38.557154 38.156018-23.107277 0-46.260603-15.309684-46.260603-38.156018C452.368366 300.994262 475.522716 285.87389 498.62897 285.87389zM283.016307 362.090758c-23.107277 0-46.402843-15.309684-46.402843-38.156018 0-22.941502 23.295566-38.061874 46.402843-38.061874 23.081695 0 38.46301 15.120372 38.46301 38.061874C321.479317 346.782098 306.098002 362.090758 283.016307 362.090758zM945.448458 606.151333c0-121.888048-123.258255-221.236753-261.683954-221.236753-146.57838 0-262.015505 99.348706-262.015505 221.236753 0 122.06508 115.437126 221.200938 262.015505 221.200938 30.66644 0 61.617359-7.609305 92.423993-15.262612l84.513836 45.786813-23.178909-76.17082C899.379213 735.776599 945.448458 674.90216 945.448458 606.151333zM598.803483 567.994292c-15.332197 0-30.807656-15.096836-30.807656-30.501688 0-15.190981 15.47546-30.477129 30.807656-30.477129 23.295566 0 38.558178 15.286148 38.558178 30.477129C637.361661 552.897456 622.099049 567.994292 598.803483 567.994292zM768.25071 567.994292c-15.213493 0-30.594809-15.096836-30.594809-30.501688 0-15.190981 15.381315-30.477129 30.594809-30.477129 23.107277 0 38.558178 15.286148 38.558178 30.477129C806.808888 552.897456 791.357987 567.994292 768.25071 567.994292z"
fill="#8a8a8a" p-id="7136"></path>
</svg>
</div>
</template>
</el-popover>
<el-popover trigger="click" placement="top" :width="180">
<p>邮箱号tone@ctbu.net.cn</p>
<div style="text-align: center; margin: 0">
<el-button size="small" type="primary" @click="copyTextwithMsg('tone@ctbu.net.cn')">复制</el-button>
</div>
<template #reference>
<div class="mx-[8px] w-[30px] h-[30px] cursor-pointer border-[1.5px] border-[#eee] flex justify-center items-center rounded-[10px] transition-all duration-300 hover:bg-[#eee]">
<svg t="1705909952800" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="8336" width="24" height="24">
<path
d="M677.288 536.903l-167.315 156.052-170.532-156.052-246.146 230.058h831.748l-247.756-230.058zM954.002 287.541v423.114l-238.1-209.145 238.1-213.969zM64.336 290.756l231.666 212.361-231.666 207.534v-419.895zM93.294 234.45l418.287 389.33 413.461-389.33h-831.748z"
fill="#8a8a8a" p-id="8337"></path>
</svg>
</div>
</template>
</el-popover>
<a class="mx-[8px] w-[30px] h-[30px] cursor-pointer border-[1.5px] border-[#eee] flex justify-center items-center rounded-[10px] transition-all duration-300 hover:bg-[#eee]" href="https://github.com/tonecn" target="_blank">
<svg t="1705909977594" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="9342" width="24" height="24">
<path
d="M511.957333 21.333333C241.024 21.333333 21.333333 240.981333 21.333333 512c0 216.832 140.544 400.725333 335.573334 465.664 24.490667 4.394667 32.256-10.069333 32.256-23.082667 0-11.690667 0.256-44.245333 0-85.205333-136.448 29.610667-164.736-64.64-164.736-64.64-22.314667-56.704-54.4-71.765333-54.4-71.765333-44.586667-30.464 3.285333-29.824 3.285333-29.824 49.194667 3.413333 75.178667 50.517333 75.178667 50.517333 43.776 75.008 114.816 53.333333 142.762666 40.789333 4.522667-31.658667 17.152-53.376 31.189334-65.536-108.970667-12.458667-223.488-54.485333-223.488-242.602666 0-53.546667 19.114667-97.322667 50.517333-131.669334-5.034667-12.330667-21.930667-62.293333 4.778667-129.834666 0 0 41.258667-13.184 134.912 50.346666a469.802667 469.802667 0 0 1 122.88-16.554666c41.642667 0.213333 83.626667 5.632 122.88 16.554666 93.653333-63.488 134.784-50.346667 134.784-50.346666 26.752 67.541333 9.898667 117.504 4.864 129.834666 31.402667 34.346667 50.474667 78.122667 50.474666 131.669334 0 188.586667-114.730667 230.016-224.042666 242.090666 17.578667 15.232 33.578667 44.672 33.578666 90.453334v135.850666c0 13.141333 7.936 27.605333 32.853334 22.869334C862.250667 912.597333 1002.666667 728.746667 1002.666667 512 1002.666667 240.981333 783.018667 21.333333 511.957333 21.333333z"
p-id="9343" fill="#8a8a8a"></path>
</svg>
</a>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,96 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter()
const showMoreOptions = ref(false);
const moreOptionsButton = ref<HTMLElement | null>(null);
const handleMoreOptionsButtonClick = () => {
showMoreOptions.value = !showMoreOptions.value;
}
const handleScreenResize = (e: MediaQueryListEvent) => {
if (e.matches)
showMoreOptions.value = false;
}
onMounted(async () => {
if (moreOptionsButton.value) {
moreOptionsButton.value.addEventListener('click', handleMoreOptionsButtonClick)
}
const mediaQueryList = window.matchMedia('(min-width: 640px)');
mediaQueryList.addEventListener('change', handleScreenResize);
if (mediaQueryList.matches)
showMoreOptions.value = false;
});
onUnmounted(async () => {
if (moreOptionsButton.value) {
moreOptionsButton.value.removeEventListener('click', handleMoreOptionsButtonClick);
}
const mediaQueryList = window.matchMedia('(min-width: 640px)');
mediaQueryList.removeEventListener('change', handleScreenResize);
})
router.afterEach(() => {
showMoreOptions.value = false;
});
</script>
<template>
<div class="z-[500] w-full fixed backdrop-blur-sm bg-white/60 dark:bg-black/10 border-b-[#eee] border-b-[1px]">
<div class="w-full box-content mx-auto py-[15px] sm:py-[22px] max-w-[1170px] flex justify-between items-center transition-all duration-200"
:class="[{ 'flex-col': showMoreOptions }]">
<!-- left header -->
<div class="px-[25px] flex items-center w-full justify-between sm:block">
<div>
<RouterLink :to="{ name: 'home' }" v-if="$route.name == 'home'">
<div class="h-[35px] text-[25px] cursor-pointer]">🍭</div>
</RouterLink>
<RouterLink :to="{ name: 'home' }" v-else>
<div
class="h-[35px] leading-[35px] font-semibold tracking-[5px] text-[#333] text-nowrap dark:text-white">
特恩(TONE)</div>
</RouterLink>
</div>
<!-- 更多选项按钮宽度小于800时出现 -->
<div class="sm:hidden block dark:fill-[#ccc]" tabindex="0" id="header-more" ref="moreOptionsButton">
<svg t="1705913460674" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="10513" width="20" height="20">
<path
d="M150.528 431.104q37.888 0 58.368 24.064t20.48 51.712l0 11.264q0 34.816-17.92 58.88t-59.904 24.064l-7.168 0q-38.912 0-61.952-21.504t-23.04-59.392l0-14.336q0-13.312 5.632-26.624t15.872-24.064 25.6-17.408 33.792-6.656l10.24 0zM519.168 431.104q37.888 0 58.368 24.064t20.48 51.712l0 11.264q0 34.816-17.92 58.88t-59.904 24.064l-7.168 0q-38.912 0-61.952-21.504t-23.04-59.392l0-14.336q0-13.312 5.632-26.624t15.872-24.064 25.6-17.408 33.792-6.656l10.24 0zM887.808 431.104q37.888 0 58.368 24.064t20.48 51.712l0 11.264q0 34.816-17.92 58.88t-59.904 24.064l-7.168 0q-38.912 0-61.952-21.504t-23.04-59.392l0-14.336q0-13.312 5.632-26.624t15.872-24.064 25.6-17.408 33.792-6.656l10.24 0z"
p-id="10514"></path>
</svg>
</div>
</div>
<!-- right header -->
<div class="sm:block" :class="[{ 'hidden': !showMoreOptions }, { 'mt-[25px]': showMoreOptions }]"
id="header-right">
<div class="flex justify-end items-center">
<RouterLink :to="{ name: 'resource' }">
<div class="whitespace-nowrap mx-[20px] cursor-pointer text-[#666] transition-all duration-200 border-b-[3px] border-transparent hover:text-black dark:text-[#ccc] dark:hover:text-white"
:class="{ 'border-b-[#e03ebf] text-black dark:text-white': $route.name === 'resource' }">资源
</div>
</RouterLink>
<RouterLink :to="{ name: 'download' }">
<div class="whitespace-nowrap mx-[20px] cursor-pointer text-[#666] transition-all duration-200 border-b-[3px] border-transparent hover:text-black dark:text-[#ccc] dark:hover:text-white"
:class="{ 'border-b-[#e03ebf] text-black dark:text-white': $route.name === 'download' }">下载
</div>
</RouterLink>
<RouterLink :to="{ name: 'blog' }">
<div class="whitespace-nowrap mx-[20px] cursor-pointer text-[#666] transition-all duration-200 border-b-[3px] border-transparent hover:text-black dark:text-[#ccc] dark:hover:text-white"
:class="{ 'border-b-[#e03ebf] text-black dark:text-white': $route.name === 'blog' || $route.name === 'blogContent' }">
博客</div>
</RouterLink>
<RouterLink :to="{ name: 'dashboard' }">
<div class="whitespace-nowrap mx-[20px] cursor-pointer text-[#666] transition-all duration-200 border-b-[3px] border-transparent hover:text-black dark:text-[#ccc] dark:hover:text-white"
:class="{ 'border-b-[#e03ebf] text-black dark:text-white': $route.name === 'login' || $route.name === 'dashboard' }">
控制台</div>
</RouterLink>
</div>
</div>
</div>
</div>
<!-- 占位盒用于解决fixed布局导致的文档流异常 -->
<div class="h-[66px] sm:h-[80px] w-full transition-all duration-200"></div>
</template>
<style scoped></style>

View File

@@ -1,229 +0,0 @@
<script setup lang='ts'>
import { onMounted, reactive, ref, type Ref } from 'vue';
import { request, type BaseResponseData } from '../../lib/request'
import { ElMessage, ElMessageBox } from 'element-plus';
const tableData: Ref<any[]> = ref([])
const dialogEditFormVisible = ref(false);
type BlogContentData = {
uuid: string,
title: string,
description: string,
created_at: Date,
src: string,
access_level: number,
visit_count: number,
like_count: number,
encrypt_p: string,
}
onMounted(async () => {
await loadTableData();
})
const loadTableData = async () => {
try {
let resourcesRes: BaseResponseData = await request.get('/console/blogs')
if (resourcesRes.code == 0) {
tableData.value = [];
tableData.value.push(...resourcesRes.data);
} else {
throw new Error(resourcesRes.message);
}
} catch (error) {
ElMessage.error(`加载失败 ${error}`)
}
}
const editForm: BlogContentData = reactive({
uuid: '',
title: '',
description: '',
created_at: new Date(),
src: '',
encrypt_p: '',
access_level: 0,
visit_count: 0,
like_count: 0
})
const editHandle = (data: any) => {
editForm.uuid = data.uuid;
editForm.title = data.title;
editForm.description = data.description;
editForm.created_at = new Date(data.created_at);
editForm.src = data.src;
editForm.access_level = data.access_level;
editForm.visit_count = data.visit_count;
dialogEditFormVisible.value = true;
}
const addHandle = () => {
editForm.uuid = '';
editForm.title = '';
editForm.description = '';
editForm.created_at = new Date();
editForm.src = '';
editForm.access_level = 10;
editForm.visit_count = 0;
editForm.like_count = 0;
dialogEditFormVisible.value = true;
}
const saveHandle = async () => {
// 表单验证
if (!editForm.title || !editForm.description || !editForm.created_at || !editForm.src || !editForm.access_level) {
return ElMessage.warning('请先完成表单')
}
try {
let res: BaseResponseData = await request.post('/console/saveBlog', {
uuid: editForm.uuid,
title: editForm.title,
description: editForm.description,
created_at: editForm.created_at,
src: editForm.src,
access_level: editForm.access_level,
})
if (res.code == 0) {
dialogEditFormVisible.value = false;
loadTableData();
if ([7, 9].includes(editForm.access_level)) {
ElMessageBox.prompt('保存成功,当前文章可访问级别为:受保护。是否立即添加密码?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputPattern: /\S+/,
inputErrorMessage: '输入不能为空或仅包含空白字符'
})
.then(async ({ value }) => {
await request.post('/console/setBlogPasswd', {
uuid: editForm.uuid,
passwd: value,
}).then((res: any) => {
if (res.code == 0) {
ElMessage({
type: 'success',
message: `密码设置成功`,
})
} else {
ElMessage({
type: 'error',
message: `密码设置失败`,
})
}
}).catch((err) => {
console.log(err)
ElMessage({
type: 'error',
message: `密码设置发生错误`,
})
})
})
.catch(() => {
ElMessage({
type: 'info',
message: '已取消',
})
})
} else {
return ElMessage.success('保存成功');
}
} else {
throw new Error(res.message);
}
} catch (error) {
return ElMessage.error(`保存失败 ${error}`);
}
}
const delHandle = async (data: { uuid: string, [key: string]: any }) => {
let { uuid } = data;
try {
let res: BaseResponseData = await request.delete('/console/blog?uuid=' + uuid);
if (res.code == 0) {
ElMessage.success('删除成功');
loadTableData();
} else {
throw new Error(res.message)
}
} catch (error) {
return ElMessage.error(`删除失败 ${error}`);
}
}
const formatTime = (row: any, _column: any, _cellValue: any, _index: any) => {
return new Date(row.created_at).toLocaleString();
}
</script>
<template>
<div class="px-[20px] py-[15px]">
<el-text>总数量{{ tableData.length }}</el-text>
<el-button type="primary" class="w-[120px] ml-[20px]" @click="addHandle">添加</el-button>
</div>
<!-- 数据列表 -->
<el-table :data="tableData" border class="w-full">
<el-table-column prop="uuid" label="uuid" width="120" show-overflow-tooltip />
<el-table-column prop="title" label="标题" width="250" />
<el-table-column prop="description" label="描述" width="300" show-overflow-tooltip />
<el-table-column prop="created_at" label="发布时间" width="160" :formatter="formatTime" />
<el-table-column prop="access_level" label="可访问级别" width="100" />
<el-table-column prop="visit_count" label="访问量" width="80" />
<el-table-column prop="like_count" label="点赞量" width="80" />
<!-- <el-table-column label="加密" width="80">
<template #default="scope">
<el-text>{{ scope.encrypt_p ? "是" : "否" }}</el-text>
</template>
</el-table-column> -->
<el-table-column fixed="right" label="操作" min-width="110">
<template #default="scope">
<el-button link type="primary" size="small" @click="editHandle(scope.row)">编辑</el-button>
<el-button link type="primary" size="small" @click="delHandle(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 编辑添加博客对话框 -->
<el-dialog v-model="dialogEditFormVisible" title="编辑" width="800">
<el-form :model="editForm" label-width="auto" style="margin: 0 30px;">
<el-form-item label="uuid">
<el-input v-model="editForm.uuid" disabled />
</el-form-item>
<el-form-item label="标题">
<el-input v-model="editForm.title" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="editForm.description" type="textarea" autosize />
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker v-model="editForm.created_at" type="datetime" placeholder="选择发布时间" />
</el-form-item>
<el-form-item label="文章链接">
<el-input v-model="editForm.src" type="textarea" autosize />
</el-form-item>
<el-form-item label="可访问级别">
<el-input-number v-model="editForm.access_level" :min="0" />
<el-tooltip placement="right">
<template #content>
1 6 - 保留<br />
7 - 列表不可见文章内容无身份验证访问<br />
8 - 列表不可见文章内容公开<br />
9 - 列表可见文章内容可无状态验证访问<br />
10 - 列表可见文章内容公开<br />
</template>
<el-text style="margin-left: 20px;" size="small">查看规则</el-text>
</el-tooltip>
</el-form-item>
<el-form-item label="访问量">
<el-input-number v-model="editForm.visit_count" :min="0" disabled />
</el-form-item>
<el-form-item label="点赞量">
<el-input-number v-model="editForm.like_count" :min="0" disabled />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogEditFormVisible = false">取消</el-button>
<el-button type="primary" @click="saveHandle">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
.el-table-col-image-preview {
display: flex;
align-items: center;
height: 80px;
width: 80px;
margin: 0 auto;
}
</style>

View File

@@ -1,476 +0,0 @@
<script setup lang="ts">
import { request } from '@/lib/request';
import { Refresh, Back, House, UploadFilled, Close } from '@element-plus/icons-vue';
import OSS, { type ObjectMeta, type PutObjectOptions } from 'ali-oss'
import { ElMessage, ElMessageBox, ElSubMenu, type UploadFile, type UploadFiles } from 'element-plus';
import { ref, onMounted, reactive } from 'vue';
// ace Editor
import ace from 'ace-builds';
import { VAceEditor } from 'vue3-ace-editor';
import 'ace-builds/src-noconflict/mode-plain_text'
import 'ace-builds/src-noconflict/mode-json'; // Load the language definition file used below
import 'ace-builds/src-noconflict/mode-markdown'
import 'ace-builds/src-noconflict/mode-javascript'
import 'ace-builds/src-noconflict/mode-typescript'
import 'ace-builds/src-noconflict/mode-html'
import 'ace-builds/src-noconflict/mode-css'
import 'ace-builds/src-noconflict/mode-vue'
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-github_dark'
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url';
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl);
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url';
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl);
import extSearchboxUrl from 'ace-builds/src-noconflict/ext-searchbox?url';
ace.config.setModuleUrl('ace/ext/searchbox', extSearchboxUrl);// searchBox
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'; // For vite
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl);
let OSSClient: OSS;
onMounted(async () => {
// 请求sts token初始化OSSClient
let sts_token_res: any = await request.get('console/ossToken');
if (sts_token_res.code == 0) {
// 请求成功
sts_token_res = sts_token_res.data;
OSSClient = new OSS({
region: sts_token_res.OSSRegion,
accessKeyId: sts_token_res.AccessKeyId,
accessKeySecret: sts_token_res.AccessKeySecret,
stsToken: sts_token_res.SecurityToken,
refreshSTSTokenInterval: sts_token_res.ExpirationSec * 1000,
bucket: sts_token_res.Bucket,
refreshSTSToken: async () => {
let sts_token_res: any = await request.get('console/ossToken');
sts_token_res = sts_token_res.data;
return {
accessKeyId: sts_token_res.AccessKeyId,
accessKeySecret: sts_token_res.AccessKeySecret,
stsToken: sts_token_res.SecurityToken,
}
},
})
} else {
throw new Error('获取OSS Token失败')
}
await loadFullFileList();
loadFileListShow();
// aceEditor 深色模式监听
// 监听主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', checkTheme);
// 初始检查
checkTheme();
})
const checkTheme = () => {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
aceEditorTheme.value = 'github_dark'
} else {
aceEditorTheme.value = 'github'
}
};
onMounted(() => {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', checkTheme);
})
// 当前目录
let prefix = ref('personal-web/');
// 完整的文件列表
const fileList: any[] = reactive([])
// 加载完整的文件列表
const loadFullFileList = async () => {
try {
// result.isTruncated ===> { marker: result.nextMarker } 用于后续实现翻页
const res = (await OSSClient.list({ "prefix": "personal-web", "max-keys": 900 }, {})).objects;
for (let i of res) {
// 初始化处理添加dir字段
(i as any).dir = (i.name as string).endsWith('/');
}
fileList.splice(0, fileList.length);
fileList.push(...res);
ElMessage.success('文件列表加载完成')
console.log(fileList)
} catch (error) {
ElMessage.error('文件列表加载失败')
}
}
// 总文件列表
for (let item of fileList) {
item.dir = item.name.endsWith('/')
}
// 文件列表可见数据
const fileListShow: any[] = reactive([]);
// 重置文件列表中,在列表中可见的数据
const loadFileListShow = () => {
fileListShow.splice(0, fileListShow.length);
const dirLength = prefix.value.indexOf('/') != -1 ? prefix.value.split('/').length - 1 : 0;
fileList.forEach((item: any) => {
const itemName: string = item.name;
// 加入当前目录中的目录
if (item.dir && itemName.startsWith(prefix.value) && itemName.split('/').length - 2 == dirLength)
fileListShow.push(item)
});
fileList.forEach((item: any) => {
const itemName: string = item.name;
// 加入当前目录中的文件
if (!item.dir && itemName.startsWith(prefix.value) && itemName.split('/').length - 1 == dirLength)
fileListShow.push(item)
});
}
// 文件被单击
const fileClick = (row: any, column: any, event: Event) => {
if (!row.dir || column.label !== '文件名')
return;
prefix.value = row.name;
loadFileListShow();
}
// 返回上级目录
const backtoLastDir = () => {
const prefixArr = prefix.value.split('/')
prefixArr.pop()
prefixArr.pop()
prefix.value = prefixArr.join('/') + (prefixArr.length > 0 ? '/' : '');
loadFileListShow();
}
// 文件列表中文件名格式化
const fileListTableNameFormatter = (row: any) => {
return (row.name as string).substring(prefix.value.length);
}
// 文件列表中上次修改时间格式化
const fileListTableLastModifiedFormatter = (row: any) => {
return row.dir ? "" : new Date(row.lastModified).toLocaleString()
}
// 文件列表中文件大小格式化
function fileListTableSizeFormatter(row: any) {
function formatterFileSize(num: number) {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
for (let i = 0; i < units.length; i++) {
if (num < 1000)
return num.toFixed(3) + units[i];
num /= 1024;
}
}
return row.dir ? "" : formatterFileSize(row.size)
}
// 退回到层级为index的某个目录
const Dirto = (index: number) => {
const prefixArr = prefix.value.split('/');
prefixArr.splice(index, prefixArr.length);
prefix.value = prefixArr.join('/') + (prefixArr.length > 0 ? '/' : '');
loadFileListShow();
}
// 删除选中的一些文件
const deleteChosenFile = () => {
}
// 文件选中状态被切换
const fileListSelectionChange = (newSelection: any[]) => {
console.log(newSelection)
}
// 长传文件的列表
let uploadFiles: UploadFiles;
const onUploadFileChange = (_uploadFile: UploadFile, _uploadFiles: UploadFiles) => {
uploadFiles = _uploadFiles;
}
// 上传文件
const uploadFile = async () => {
if (uploadFiles.length < 1) {
return ElMessage.error('请选择需要上传的文件')
}
// console.log(uploadFiles)
// console.log(prefix.value)
isUploading.value = true;
let uploadSuccessCount = 0;
for (let i of uploadFiles) {
if (i.status == 'success' || i.status == 'uploading')
continue;
try {
i.status = 'uploading';
// 采用分片上传方式,以获取上传进度
const name = prefix.value + i.raw!.name;
const options: any = {
// 获取分片上传进度、断点和返回值。
progress: (p: any, cpt: any, res: any) => {
// console.log(p, cpt, res);
i.percentage = +(p * 100).toFixed(2);
if (p == 1) {
i.status = 'success';
uploadSuccessCount++;
}
},
// 设置并发上传的分片数量。
parallel: 4,
// 设置分片大小。默认值为256 KB最小值为100 KB。
partSize: 256 * 1024,
}
if (i.raw!.size > 10 * 1024 * 1024 * 1024)// 大于10GB的文件分成50M
options.partSize = 50 * 1024 * 1024;
else if (i.raw!.size > 10 * 1024 * 1024)// 大于100MB的文件分成1MB
options.partSize = 1 * 1024 * 1024;
await OSSClient.multipartUpload(name, i.raw, options);
} catch (error) {
i.status = 'fail';
ElMessage.error(`上传失败 ${error}`)
}
}
if (uploadSuccessCount) {
ElMessage.success(`${uploadSuccessCount} 个文件上传完成`);
await loadFullFileList();
loadFileListShow();
} else
ElMessage.error(`文件上传失败`)
isUploading.value = false;
}
// 文件列表操作:删除某个文件
const fileListHandleDelete = async (row: any) => {
ElMessageBox.confirm(
'是否要删除该文件?',
'警告',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
await OSSClient.delete(row.name);
ElMessage.success('删除完成');
await loadFullFileList();
loadFileListShow();
} catch (error) {
ElMessage.error('删除失败');
}
})
}
// 文件列表操作:下载某个文件
const fileListHandleDownload = async (row: any) => {
window.open(row.url);
}
// 文件列表操作:重命名某个文件
const fileListHandleRename = async (row: any) => {
ElMessageBox.prompt('请输入新的文件名', '提示', {
confirmButtonText: '提交',
cancelButtonText: '取消',
inputValue: row.name
}).then(async ({ value }) => {
try {
await OSSClient.copy(value, row.name);
await OSSClient.delete(row.name);
ElMessage.success('重命名成功');
await loadFullFileList()
loadFileListShow();
} catch (error) {
ElMessage.error('重命名失败');
}
}).catch(() => {
// 已取消
})
}
// 文件列表操作:编辑某个文件
const editableFileTypes = new Map([
['.txt', 'text'],
['.md', 'markdown'],
['.json', 'json'],
['.js', 'javascript'],
['.ts', 'typescript'],
['.vue', 'vue'],
['.html', 'html'],
['.css', 'css'],
]);
const fileListHandleEdit = async (row: ObjectMeta) => {
aceEditorConfig.lang = '';
for (let [key, value] of editableFileTypes) {
if (row.name.endsWith(key)) {
aceEditorConfig.lang = value;
break;
}
}
if (!aceEditorConfig.lang)
return ElMessage.warning('不支持的文件格式')
aceEditorFileInfo.url = row.url;
aceEditorFileInfo.name = row.name;
try {
const res = await fetch(row.url);
if (!res.ok)
return ElMessage.error('请求失败')
const blob = await res.blob();
const text = await blob.text();
aceEditorContent.text = text;
aceEditorContent.textBak = text;
aceEditorContent.type = blob.type;
aceEditorShow.value = true;
} catch (error) {
ElMessage.error('打开失败')
}
}
const dialogUploadFileShow = ref(false);
const isUploading = ref(false);
const aceEditorContent = reactive({
text: '',
textBak: '',
type: ''
});
const aceEditorShow = ref(false);
const aceEditorFileInfo = reactive({ url: "", name: "" })
const aceEditorConfig = reactive({
lang: 'textplain',
})
const closeAceEditor = () => {
if (aceEditorContent.text == aceEditorContent.textBak)
return aceEditorShow.value = false;
ElMessageBox.confirm('文件未保存,是否退出?', '警告', {
confirmButtonText: '退出',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
aceEditorShow.value = false;
}).catch(() => {
})
}
const resetAceEditorContent = () => {
if (aceEditorContent.text == aceEditorContent.textBak)
return ElMessage.info('文件未修改')
aceEditorContent.text = aceEditorContent.textBak;
ElMessage.success('重置完成')
}
const saveAceEditorContentLoading = ref(false);
const saveAceEditorContent = async () => {
if (aceEditorContent.text == aceEditorContent.textBak)
return ElMessage.info('文件未修改')
saveAceEditorContentLoading.value = true;
try {
const blob = new Blob([aceEditorContent.text], { type: aceEditorContent.type });
const file = new File([blob], aceEditorFileInfo.name, { type: aceEditorContent.type });
await OSSClient.put(aceEditorFileInfo.name, file);
ElMessage.success('保存成功');
aceEditorContent.textBak = aceEditorContent.text;
} catch (error) {
ElMessage.success('保存失败');
} finally {
saveAceEditorContentLoading.value = false;
}
}
const aceEditorTheme = ref('github');
</script>
<template>
<div class="container w-full">
<div class="flex items-center p-[10px] flex-wrap">
<el-button class="mr-[-15px]" @click="() => { loadFullFileList(); loadFileListShow(); }" circle link>
<el-icon>
<Refresh />
</el-icon>
</el-button>
<el-button @click="backtoLastDir" circle link>
<el-icon>
<Back />
</el-icon>
</el-button>
<el-text>当前目录</el-text>
<el-icon @click="Dirto(0)" class="cursor-pointer hover:text-[#222] mr-[5px]">
<House />
</el-icon>
<span v-for="i, key of prefix.split('/')" class="text-[#666] dark:text-[#bbb]">
<span v-if="i != ''" class="mx-[3px]">/</span>
<span @click="Dirto(key + 1)" class="cursor-pointer hover:text-[#222] dark:hover:text-[#fff]">{{ i
}}</span>
</span>
<span class="mx-[3px] text-[#666]">/</span>
</div>
</div>
<div class="container w-full">
<el-button-group class="pl-[10px]">
<el-button @click="dialogUploadFileShow = true">上传</el-button>
<el-button>下载</el-button>
<el-button>编辑</el-button>
<el-button type="danger" :disabled="false" @click="deleteChosenFile">删除</el-button>
</el-button-group>
</div>
<!-- 文件列表 -->
<el-table :data="fileListShow" @cell-click="fileClick" @selection-change="fileListSelectionChange" border
class="mt-[10px]">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="文件名" :formatter="fileListTableNameFormatter" />
<el-table-column prop="lastModified" :formatter="fileListTableLastModifiedFormatter" label="最后修改时间"
width="180" />
<el-table-column prop="size" :formatter="fileListTableSizeFormatter" label="文件大小" width="130" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button v-if="!scope.row.dir" size="small" text
@click.prevent="fileListHandleEdit(scope.row)">编辑</el-button>
<el-button v-if="!scope.row.dir" size="small" text style="margin-left: 0;"
@click.prevent="fileListHandleDownload(scope.row)">下载</el-button>
<el-dropdown placement="bottom-end" v-if="!scope.row.dir">
<el-button size="small" text>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button v-if="!scope.row.dir" size="small" text
@click.prevent="fileListHandleRename(scope.row)">重命名</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button v-if="!scope.row.dir" size="small" text type="danger" style="margin-left: 0;"
@click.prevent="fileListHandleDelete(scope.row)">删除</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!-- 文件上传对话框 -->
<el-dialog v-if="dialogUploadFileShow" v-model="dialogUploadFileShow" :multiple="true" title="上传文件"
class="min-w-[300px]">
<el-upload drag multiple :auto-upload="false" :on-change="onUploadFileChange" :disabled="isUploading">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖动文件到此处或 <em>单击选择</em>
</div>
<template #tip>
<div class="el-upload__tip">任何小于5GB的文件</div>
</template>
</el-upload>
<el-button @click="uploadFile" :loading="isUploading" class="mt-[10px]">开始上传</el-button>
</el-dialog>
<!-- 在线文本编辑 -->
<transition name="el-fade-in-linear">
<div v-if="aceEditorShow"
class="fixed w-full h-full inset-0 z-[2000] bg-[#00000066] flex justify-center items-center"
@click="closeAceEditor">
<div class="w-3/4 h-3/4 bg-white dark:bg-[#141414] p-[15px] rounded-[5px] flex flex-col" @click.stop>
<div class="w-full flex items-center justify-between">
<span>正在编辑 - /{{ aceEditorFileInfo.name }}</span>
<el-button circle text @click="closeAceEditor">
<el-icon>
<Close />
</el-icon>
</el-button>
</div>
<div class="w-full flex items-center gap-[15px] pb-[15px]">
<span>语言
<el-select v-model="aceEditorConfig.lang" style="width: 130px;margin-left: 8px;">
<el-option v-for="[key, value] of editableFileTypes" :key="value" :label="value"
:value="value"></el-option>
</el-select>
</span>
</div>
<v-ace-editor v-model:value="aceEditorContent.text" :lang="aceEditorConfig.lang" :theme="aceEditorTheme"
class="w-full flex-1" />
<div class="flex justify-end mt-[10px]">
<el-button @click="resetAceEditorContent">重置</el-button>
<el-button @click="saveAceEditorContent" :loading="saveAceEditorContentLoading"
type="primary">保存</el-button>
</div>
</div>
</div>
</transition>
</template>

View File

@@ -1,198 +0,0 @@
<script setup lang='ts'>
import { onMounted, reactive, ref, type Ref } from 'vue';
import { request, type BaseResponseData } from '../../lib/request'
import { ElMessage } from 'element-plus';
type ResourceData = {
uuid: string,
type: string,
recommand: number,
title: string,
describe: string,
icon_src: string,
addition: string,
src: string,
}
onMounted(async () => {
await loadTableData();
})
const tableData: Ref<any[]> = ref([])
const dialogEditFormVisible = ref(false);
const loadTableData = async () => {
try {
let resourcesRes: BaseResponseData = await request.get('/console/resources')
if (resourcesRes.code == 0) {
tableData.value = [];
tableData.value.push(...resourcesRes.data);
} else {
throw new Error(resourcesRes.message);
}
} catch (error) {
ElMessage.error(`加载失败 ${error}`)
}
}
const editForm: ResourceData = reactive({
uuid: '',
type: '',
recommand: 1,
title: '',
describe: '',
icon_src: '',
addition: '',
src: '',
})
const openEditFormSrc = () => { window.open(editForm.src); }
const editHandle = (data: ResourceData) => {
editForm.uuid = data.uuid;
editForm.type = data.type;
editForm.recommand = +data.recommand;
editForm.title = data.title;
editForm.describe = data.describe;
editForm.icon_src = data.icon_src;
editForm.src = data.src;
editForm.addition = JSON.stringify(data.addition);
dialogEditFormVisible.value = true;
}
const addHandle = () => {
editForm.uuid = '';
editForm.type = '';
editForm.recommand = 1;
editForm.title = '';
editForm.describe = '';
editForm.icon_src = '';
editForm.src = '';
editForm.addition = '';
dialogEditFormVisible.value = true;
}
const saveHandle = async () => {
// 表单验证
if (!editForm.addition || !editForm.describe || !editForm.icon_src || !editForm.recommand || !editForm.src || !editForm.title || !editForm.type) {
return ElMessage.warning('请先完成表单')
}
try {
let res: BaseResponseData = await request.post('/console/saveResource', {
uuid: editForm.uuid,
type: editForm.type,
recommand: editForm.recommand,
title: editForm.title,
describe: editForm.describe,
icon_src: editForm.icon_src,
addition: editForm.addition,
src: editForm.src,
})
if (res.code == 0) {
dialogEditFormVisible.value = false;
loadTableData();
return ElMessage.success('保存成功');
} else {
throw new Error(res.message);
}
} catch (error) {
return ElMessage.error(`保存失败 ${error}`);
}
}
const delHandle = async (data: { id: string, [key: string]: any }) => {
let { uuid } = data;
try {
let res: BaseResponseData = await request.delete('/console/resource?uuid=' + uuid);
if (res.code == 0) {
ElMessage.success('删除成功');
loadTableData();
} else {
throw new Error(res.message)
}
} catch (error) {
return ElMessage.error(`删除失败 ${error}`);
}
}
const formatTime = (row: any, _column: any, _cellValue: any, _index: any) => {
return new Date(row.created_at).toLocaleString();
}
</script>
<template>
<div class="py-[15px] px-[20px]">
<el-text>总数量{{ tableData.length }}</el-text>
<el-button type="primary" style="width: 120px;margin-left: 20px;" @click="addHandle">添加</el-button>
</div>
<!-- 数据列表 -->
<el-table :data="tableData" border class="w-full">
<el-table-column prop="uuid" label="uuid" width="60" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="80" sortable>
<template #default="scope">
{{ scope.row.type == 'resource' ? '资源' : scope.row.type == 'download' ? '下载' : '未知' }}
</template>
</el-table-column>
<el-table-column prop="recommand" label="推荐" width="60" />
<el-table-column prop="title" label="标题" width="130" />
<el-table-column prop="describe" label="描述" width="200" />
<el-table-column prop="icon_src" label="图标" width="110">
<template #default="scope">
<div class="el-table-col-image-preview">
<el-image :src="scope.row.icon_src" />
</div>
</template>
</el-table-column>
<el-table-column prop="addition" label="附加样式" width="300">
<template #default="scope">
{{ scope.row.addition }}
</template>
</el-table-column>
<el-table-column prop="src" label="资源" width="180" />
<el-table-column fixed="right" label="操作" min-width="110">
<template #default="scope">
<el-button link type="primary" size="small" @click="editHandle(scope.row)">编辑</el-button>
<el-button link type="primary" size="small" @click="delHandle(scope.row)">删除</el-button>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" :formatter="formatTime" />
</el-table>
<!-- 编辑添加资源对话框 -->
<el-dialog v-model="dialogEditFormVisible" title="编辑" width="800">
<el-form :model="editForm" label-width="auto" class="mx-[30px]">
<el-form-item label="uuid">
<el-input v-model="editForm.uuid" disabled />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="editForm.type" placeholder="请选择类型">
<el-option label="资源" value="resource" />
<el-option label="下载" value="download" />
</el-select>
</el-form-item>
<el-form-item label="推荐程度">
<el-input-number v-model="editForm.recommand" :min="1" />
</el-form-item>
<el-form-item label="标题">
<el-input v-model="editForm.title" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="editForm.describe" type="textarea" autosize />
</el-form-item>
<el-form-item label="图标链接">
<el-input v-model="editForm.icon_src" />
<img :src="editForm.icon_src" class="w-[80px] h-[80px] mt-2 shadow rounded-[4px] bg-gray-100"
alt="链接无效">
</el-form-item>
<el-form-item label="资源链接">
<el-input v-model="editForm.src" />
<el-button class="mt-2" @click="openEditFormSrc">前往链接</el-button>
</el-form-item>
<el-form-item label="附加样式">
<el-input v-model="editForm.addition" type="textarea" autosize />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogEditFormVisible = false">取消</el-button>
<el-button type="primary" @click="saveHandle">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
.el-table-col-image-preview {
display: flex;
align-items: center;
height: 80px;
width: 80px;
margin: 0 auto;
}
</style>

View File

@@ -1,44 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import md5 from 'md5'
import copyText from '@/lib/copyText'
import { ElMessage } from 'element-plus'
const randomNum = ref(0)
const uuid = ref('')
const generateRandom = () => {
randomNum.value = Math.random()
}
const generateUUID = () => {
uuid.value = md5(`${Math.random()}${Date.now()}`);
}
</script>
<template>
<el-col class="p-[20px]">
<el-row>
<el-text>随机数生成</el-text>
</el-row>
<el-row>
<el-input style="width: 230px;" v-model="randomNum" />
</el-row>
<el-row>
<el-button-group>
<el-button style="width: 115px;" @click="generateRandom" type="primary">生成</el-button>
<el-button style="width: 115px;"
@click="() => { copyText(randomNum + ''); ElMessage.success('复制成功') }">复制</el-button>
</el-button-group>
</el-row>
<el-row class="mt-[15px]">
<el-text>32位UUID生成</el-text>
</el-row>
<el-row>
<el-input style="width: 230px;" v-model="uuid" />
</el-row>
<el-row>
<el-button-group>
<el-button style="width: 115px;" @click="generateUUID" type="primary">生成</el-button>
<el-button style="width: 115px;"
@click="() => { copyText(uuid + ''); ElMessage.success('复制成功') }">复制</el-button>
</el-button-group>
</el-row>
</el-col>
</template>

View File

@@ -1,27 +0,0 @@
/**
*
* @param textToCopy 需要被复制的文本
* @returns 成功返回 true, 不成功返回 false
*/
const copyText = (textToCopy: string): boolean => {
// 创建一个<textarea>元素,将文本放入其中
const textArea = document.createElement("textarea");
textArea.value = textToCopy;
// 将<textarea>元素添加到DOM中
document.body.appendChild(textArea);
// 选择<textarea>元素中的文本
textArea.select();
let copyStatus = false; // true成功, false失败
try {
// 尝试将选定的文本复制到剪贴板
document.execCommand("copy");
copyStatus = true;
} catch (err) {
console.error("复制文本失败:", err);
}
// 从DOM中移除<textarea>元素
document.body.removeChild(textArea);
return copyStatus;
}
export default copyText;

View File

@@ -1,9 +0,0 @@
function formateTimes(times: number) {
if (times < 10000)// 一万以内直接显示
return times;
if (times < 10000 * 10000)// 一亿以内加上 万
return (times / 10000).toFixed(1) + '万'
return (times / 10000 / 10000).toFixed(1) + '亿'
}
export { formateTimes };

View File

@@ -1,28 +0,0 @@
import axios from "axios";
type BaseResponseData = {
code: number,
message: string,
data: any
};
axios.defaults.baseURL = "http://localhost:23500";
// axios.defaults.baseURL = "https://tonesc.cn/apis";
axios.interceptors.response.use((response) => {
if (response.data && response.data.code == -5) {
// auth error
localStorage.removeItem('jwtToken');
window.location.reload()
}
// 确保响应数据符合ResponseData接口的结构
return response.data;
});
axios.interceptors.request.use((request) => {
if (localStorage.getItem('jwtToken')) {
request.headers['Authorization'] = 'Bearer ' + localStorage.getItem('jwtToken');
}
return request;
})
export type { BaseResponseData };
export { axios as request };

View File

@@ -1,28 +0,0 @@
function timestampToRelativeWeekday(timestamp: string | number) {
if (timestamp === undefined || timestamp === null) {
throw new Error('Timestamp cannot be undefined or null');
}
const date = new Date(+timestamp);
const today = new Date();
today.setHours(0, 0, 0, 0); // 设置为今天的零点,以便于比较
// 判断是否为明天
const date0000 = new Date(date);
date0000.setHours(0, 0, 0, 0);
const isTomorrow = date0000.getTime() === today.getTime() + 24 * 60 * 60 * 1000;
const isToday = date0000.getTime() === today.getTime();
// 获取星期几注意JavaScript的getDay()返回值是0周日到6周六
const weekday = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][date.getDay()];
if (isToday) {
return '今天';
} else if (isTomorrow) {
return '明天';
} else {
return weekday;
}
}
export { timestampToRelativeWeekday }

View File

@@ -1,15 +0,0 @@
import './assets/main.css'
import './assets/tailwind.css'
import { createApp } from 'vue'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

View File

@@ -1,68 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/resource',
name: 'resource',
component: () => import('../views/Resource.vue')
},
{
path: '/download',
name: 'download',
component: () => import('../views/Download.vue')
},
{
path: '/blog',
name: 'blog',
component: () => import('../views/Blog.vue')
},
{
path: '/blogContent/:uuid',
name: 'blogContent',
component: () => import('../views/BlogContent.vue'),
},
{
path: '/console',
name: 'console',
component: () => import('../views/Console/Console.vue'),
children: [
{
path: 'login',
name: 'login',
component: () => import('../views/Console/Login.vue')
},
{
path: 'dashboard',
name: 'dashboard',
component: () => import('../views/Console/Dashboard.vue')
}
],
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
component: () => import('../views/NotFound.vue')
}
]
})
router.beforeEach((to, from, next) => {
const isAuthenticated = !!localStorage.getItem('jwtToken');
if (to.name === 'dashboard' && !isAuthenticated) {
return next({ name: 'login' });
} else if (to.name === 'login' && isAuthenticated) {
return next({ name: 'dashboard' });
}
next()
})
export default router

View File

@@ -1,43 +0,0 @@
<script setup lang='ts'>
import { request, type BaseResponseData } from '@/lib/request';
import { onMounted, reactive, ref } from 'vue';
import { formateTimes } from '@/lib/formateTimes';
const loadStatus = ref(0);
const blogList: any[] = reactive([]);
onMounted(async () => {
try {
const blogListRes: BaseResponseData = await request.get('/blogList');
if (blogListRes.code == 0) {
blogList.push(...blogListRes.data);
loadStatus.value = 1;
} else {
throw new Error(blogListRes.message);
}
} catch (error) {
console.error(error)
loadStatus.value = -1;
}
})
</script>
<template>
<div class="bg-default-bg fixed inset-0 w-full h-full -z-10 dark:bg-[#222]"></div>
<div class="flex flex-col max-w-[800px] my-[30px] mx-auto px-[20px]">
<el-empty description="加载失败,刷新后重试" style="margin: 0 auto;" v-if="loadStatus == -1" />
<div class="gap-[30px] flex flex-col" v-else>
<div class="text-center mt-[20px] mb-[300px] dark:text-white" v-if="loadStatus == 0">加载中请稍后...</div>
<el-empty description="暂无数据" style="margin: 0 auto;" v-if="loadStatus == 1 && blogList.length == 0" />
<div class="mx-auto max-w-[400px] w-full flex flex-col items-start" v-for="item of blogList">
<a class="text-[26px] font-semibold cursor-pointer block hover:underline dark:text-white text-[#333]"
:href="`/blogContent/${item.uuid}`" target="_blank">
{{ item.title }}
<span v-if="item.access_level == 9"
class="text-[10px] text-[#666] dark:text-[#ccc] font-normal border border-[#d7d9de] py-[2px] px-[4px] rounded-[10px] bg-[#ebecf0] dark:border-[#999] dark:!bg-[#ffffff22]">受密码保护</span>
</a>
<div class="text-[#666] dark:text-[#ccc]">{{ item.description }}</div>
<div class="text-[#888] mt-[15px] text-[14px]">{{ new Date(item.created_at).toLocaleString() }} ·
{{ formateTimes(item.visit_count) }} 次访问</div>
</div>
</div>
</div>
</template>

View File

@@ -1,187 +0,0 @@
<script setup lang='ts'>
import { request, type BaseResponseData } from '@/lib/request';
import { onMounted, onUnmounted, ref, type Ref } from 'vue';
import { useRoute } from 'vue-router'
import { Marked } from 'marked';
import { markedHighlight } from "marked-highlight";
import hljs from 'highlight.js';
import "highlight.js/styles/xcode.css";
import BlogContentToolBar from '@/components/Blog/BlogContentToolBar.vue';
import BlogComment from '@/components/Blog/BlogComment.vue';
type BlogInfo = {
visit_count: number,
title: string,
created_at: string,
like_count: number,
description: string
}
const loadStatus = ref(0);// 0加载中 -1加载失败 1加载成功
const loadStatusDescription = ref('出错啦,返回到上一个界面重试吧');
const route = useRoute();
const blogContent = ref('');
const blogInfo: Ref<BlogInfo> = ref({
visit_count: 0,
title: '',
created_at: '',
like_count: 0,
description: ''
});
const blogCommentReload = ref(false);
const marked = new Marked(
markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);
onMounted(async () => {
const bloguuid = route.params.uuid;
if (bloguuid.length != 32) {
return loadStatus.value = -1;
}
try {
let blogContentRes: BaseResponseData = await request.get(`/blogContent?bloguuid=${bloguuid}` + (window.location.href.indexOf('?') != -1 ? "&" + window.location.href.split('?')[1] : ""));
console.log(blogContent.value)
if (blogContentRes.code == 0) {
try {
blogContent.value = await marked.parse(decodeURIComponent(escape(atob(blogContentRes.data.data))))
blogInfo.value = blogContentRes.data.info;
loadStatus.value = 1;
// 标题
document.title = blogContentRes.data.info.title + ' —— 特恩(TONE)';
} catch (error) {
throw error
}
} else if (blogContentRes.code == -4001) {
// 不可见或不存在
loadStatus.value = -1;
loadStatusDescription.value = '文章不可见或不存在'
} else if (blogContentRes.code == -4002) {
// 文章加密
loadStatus.value = -1;
loadStatusDescription.value = '文章被加密啦,请联系发布者'
} else if (blogContentRes.code == -4003) {
loadStatus.value = -2;
loadStatusDescription.value = '密钥好像有点问题,请联系发布者'
} else {
throw new Error(blogContentRes.message);
}
} catch (error) {
console.error('请求博客内容发生错误 ', error);
loadStatusDescription.value = '加载失败,刷新后重试';
loadStatus.value = -1;
}
})
onUnmounted(() => {
document.title = '特恩(TONE)';
})
</script>
<template>
<div class="bg-default-bg dark:bg-[#222] fixed inset-0 w-full h-full -z-10"></div>
<div class="flex flex-col max-w-[700px] my-[50px] mx-auto px-[20px]">
<div class="mx-auto dark:text-white" v-if="loadStatus == 0">加载中请稍后...</div>
<div v-if="loadStatus < 0" class="flex justify-center items-center sm:h-[calc(100vh-190px)] h-[calc(100vh-180px)] min-h-[260px]">
<el-empty class="mb-[60px]" :description="loadStatusDescription" />
</div>
<div v-if="loadStatus == 1">
<div>
<h1 class="text-center text-[28px] font-semibold dark:text-white text-[#222]">{{ blogInfo.title }}</h1>
<p class="my-[15px] mx-auto text-[14px] text-[#888] dark:text-[#ccc] text-center">发布于 {{
new Date(blogInfo.created_at).toLocaleString() }} {{ blogInfo.like_count }} 点赞</p>
<!-- TODO <p class="mt-[-10px] mx-auto text-[14px] text-[#888] dark:text-[#ccc] text-center whitespace-pre">
{{ blogInfo.visit_count }} 访问
{{ blogInfo.like_count }} 点赞
</p> -->
</div>
<div v-html="blogContent" class="w-full overflow-x-scroll" id="blogContentContainer"></div>
</div>
<BlogComment v-if="loadStatus == 1" v-model="blogCommentReload" />
</div>
<BlogContentToolBar v-if="loadStatus == 1" @comment-success="blogCommentReload = true;" />
</template>
<style>
/* markdown CSS */
#blogContentContainer img {
@apply w-full my-[5px];
}
#blogContentContainer p {
@apply text-[#555] dark:text-[#ddd] my-[10px];
}
#blogContentContainer p code {
@apply bg-[#dedede] dark:bg-[#ffffff44] py-[1px] px-[3px] rounded-[3px];
}
#blogContentContainer blockquote {
@apply my-[16px] pl-[25px] border-[4px_solid_#ddd];
}
#blogContentContainer blockquote p {
@apply text-[#888];
}
#blogContentContainer pre {
@apply rounded-[5px] overflow-hidden;
}
#blogContentContainer pre code {
@apply overflow-x-scroll;
}
#blogContentContainer h1,
#blogContentContainer h2,
#blogContentContainer h3,
#blogContentContainer h4,
#blogContentContainer h5,
#blogContentContainer h6 {
@apply font-[800] text-[#333] dark:text-[#fff] mt-[20px];
}
#blogContentContainer h1 {
@apply text-[28px] border-b-[#ddd] dark:border-b-[#999] border-b;
}
#blogContentContainer h2 {
@apply text-[24px] border-b-[#ddd] dark:border-b-[#999] border-b;
}
#blogContentContainer h3 {
@apply text-[22px];
}
#blogContentContainer h4 {
@apply text-[20px];
}
#blogContentContainer h5 {
@apply text-[18px];
}
#blogContentContainer h6 {
@apply text-[16px];
}
#blogContentContainer pre {
@apply text-[12px] rounded-[8px] shadow;
}
#blogContentContainer table thead th {
@apply border-[2px];
}
#blogContentContainer table td {
@apply border-[1px] p-[5px];
}
#blogContentContainer a {
@apply dark:text-white;
}
</style>

View File

@@ -1,6 +0,0 @@
<script setup lang="ts">
</script>
<template>
<router-view></router-view>
</template>

View File

@@ -1,105 +0,0 @@
<script setup lang="ts">
import { Menu as IconMenu, Document, Back, Tools, Files } from '@element-plus/icons-vue'
import Resources from '../../components/Console/Resources.vue'
import Blogs from '../../components/Console/Blogs.vue'
import Utils from '../../components/Console/Utils.vue'
import FileOnline from '../../components/Console/FileOnline.vue'
import { shallowRef, ref, onMounted, onUnmounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { request } from '@/lib/request'
const tabComponent = shallowRef(Resources);
const menuCollapse = ref(false)
const handleResize = () => {
if (window.matchMedia('(max-width: 768px)').matches) {
menuCollapse.value = true;
} else {
menuCollapse.value = false;
}
};
const logout = () => {
ElMessageBox.confirm(
'是否要退出登录?',
'警告',
{
confirmButtonText: '退出',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
localStorage.clear()
ElMessage.success('退出成功')
setTimeout(() => {
window.location.reload();
}, 1000);
})
}
onMounted(async () => {
handleResize()
window.addEventListener('resize', handleResize);
const refreshToken = async () => {
// 判断jwt过期时间并定期刷新
if (localStorage.getItem('jwtToken')) {
const binaryString = atob(localStorage.getItem('jwtToken')!.split('.')[1]);
const jwtPayload = JSON.parse(binaryString);
if ((jwtPayload.exp - Math.floor(Date.now() / 1000)) < 0) {
// token已过期
localStorage.clear()
window.location.reload()
}
if ((jwtPayload.exp - Math.floor(Date.now() / 1000)) / (jwtPayload.exp - jwtPayload.iat) < 0.3) {
// 重新获取token
console.log("token有效期 ", (jwtPayload.exp - Math.floor(Date.now() / 1000)) / (jwtPayload.exp - jwtPayload.iat), "不足%30正在重新获取Token")
let res: any = await request.get('/console/loginStatus');
if (res.code == 0) {
localStorage.setItem('jwtToken', res.data.token)
}
}
}
}
setInterval(refreshToken, 1000 * 60 * 30);// 30 分钟判定一次token有效期
refreshToken()
})
onUnmounted(async () => {
window.removeEventListener('resize', handleResize);
})
</script>
<template>
<div class="flex w-[100vw]">
<el-menu default-active="1" :collapse="menuCollapse">
<el-menu-item index="1" @click="tabComponent = Resources">
<el-icon><icon-menu /></el-icon>
<span style="width: 140px;">资源及下载</span>
</el-menu-item>
<el-menu-item index="2" @click="tabComponent = Blogs">
<el-icon>
<document />
</el-icon>
<span>博客管理</span>
</el-menu-item>
<el-menu-item index="3" @click="tabComponent = FileOnline">
<el-icon>
<Files />
</el-icon>
<span>文件管理</span>
</el-menu-item>
<el-menu-item index="4" @click="tabComponent = Utils">
<el-icon>
<Tools />
</el-icon>
<span>实用工具</span>
</el-menu-item>
<el-menu-item @click="logout">
<el-icon>
<Back />
</el-icon>
<span>退出登录</span>
</el-menu-item>
</el-menu>
<div class="flex-1 overflow-x-scroll sm:min-h-[calc(100vh-90px)] min-h-[calc(100vh-77px)]">
<KeepAlive>
<component :is="tabComponent" />
</KeepAlive>
</div>
</div>
</template>

View File

@@ -1,81 +0,0 @@
<script setup lang='ts'>
import { ElMessage } from 'element-plus';
import { onMounted, reactive, ref } from 'vue';
import { request, type BaseResponseData } from '@/lib/request';
const containerHeight = ref('800px');
const isCaptchaShow = ref(false);
const loginStatus = ref(false);
const formData = reactive({
username: '',
password: ''
})
const login = async () => {
if (!formData.username || !formData.password) {
return ElMessage.warning('请填写账户名和密码')
}
try {
loginStatus.value = true;
let loginRes: BaseResponseData = await request.post('/console/login', {
username: formData.username,
password: formData.password,
session: localStorage.getItem('captcha-session')
})
switch (loginRes.code) {
case 0:
// 成功
localStorage.setItem('jwtToken', loginRes.data.token);
ElMessage.success('登录成功');
return setTimeout(() => {
window.location.reload();
}, 1000);
case -1:
// 参数缺失
return ElMessage.error('参数缺失');
case -3:
// 服务器错误
return ElMessage.error('服务器错误');
case -6000:
// 用户不存在
return ElMessage.error('用户不存在');
case -6001:
// 账户名或密码错误
return ElMessage.error('账户名或密码错误');
default:
return ElMessage.error(`未知错误 ${loginRes.message}`)
}
} catch (error) {
return ElMessage.error(`未知错误 ${error}`)
} finally {
loginStatus.value = false;
}
}
onMounted(async () => {
containerHeight.value = window.innerHeight > 500 ? window.innerHeight - 90 + 'px' : '390px';
})
</script>
<template>
<div class="bg-default-bg fixed inset-0 w-full h-full -z-10 dark:bg-[#222]"></div>
<div
class="flex flex-col items-center justify-center mx-auto sm:h-[calc(100vh-90px)] h-[calc(100vh-77px)] px-[20px] max-w-[280px] min-h-[400px]">
<h1 class="text-center text-[36px] font-medium cursor-default text-black dark:text-white">登录到控制台</h1>
<el-popover placement="bottom" :width="300" trigger="click">
<p>控制台是特恩(TONE)网页中的一个控制器界面如果您是管理员可通过登录后编辑本网页中的内容资源工具日记等详情请见<strong style="cursor: pointer;"
@click="">特恩(TONE)控制台使用协议</strong></p>
<template #reference>
<h3 class="text-center cursor-pointer font-semibold mt-[10px] text-[#333] dark:text-[#ccc]">控制台是什么</h3>
</template>
</el-popover>
<div class="w-full flex flex-col items-center">
<el-input v-model="formData.username" class="w-full h-[35px] text-[16px] mt-[20px]" placeholder="请输入账户名"
clearable>
</el-input>
<el-input v-model="formData.password" show-password class="w-full h-[35px] text-[16px] mt-[10px]"
placeholder="密码" @keyup.enter="login">
</el-input>
<el-button
class="mt-[12px] mb-[120px] w-full h-[35px] font-bold login-button hover:!bg-white hover:!border-gray-300 hover:!text-gray-800"
@click="login" :loading="loginStatus">登录</el-button>
</div>
</div>
</template>

View File

@@ -1,75 +0,0 @@
<script setup lang="ts">
import { request, type BaseResponseData } from '@/lib/request';
import Agreement from '@/components/Common/Agreement.vue';
import { ref, onMounted, reactive } from 'vue';
let showAgreement = ref(false);
let loadStatus = ref(0);// 0加载中 1加载成功 2加载失败
let ResourceDatas: any[] = reactive([])
onMounted(async () => {
// 用于获取数据的函数
try {
let res: BaseResponseData = await request.get('/resourceList?type=download');
if (res && res.code == 0) {
loadStatus.value = 1;
ResourceDatas.push(...res.data)
} else {
throw new Error(res.message)
}
} catch (error) {
console.error(error)
loadStatus.value = 2;
}
})
</script>
<template>
<div class="mx-auto flex flex-col items-center">
<div class="bg-default-bg fixed inset-0 w-full h-full -z-10 dark:bg-[#222]"></div>
<div
class="mt-[30px] sm:mt-[80px] text-[30px] sm:text-[42px] cursor-default transition-all duration-500 dark:text-[#ddd] text-[#333]">
一些可以直接下载的工具
</div>
<div class="mt-[20px] text-[#666] cursor-default text-[12px] px-[20px] dark:text-[#ccc]" v-if="loadStatus != 2">
请在浏览此部分内容前阅读并同意
<a class="text-[#222] cursor-pointer underline] dark:text-[#fff]"
@click="showAgreement = true">网站使用协议</a>继续使用或浏览表示您接受协议条款
</div>
<div class="mt-[80px] cursor-default mb-[380px] text-center dark:text-[#ddd]" v-if="loadStatus == 2">加载失败请刷新界面重试
</div>
<div class="mt-[80px] cursor-default mb-[380px] text-center dark:text-[#ddd]" v-if="loadStatus == 0">加载中请稍后...
</div>
<div
class="mt-[25px] sm:mt-[50px] mx-auto flex justify-center items-start flex-wrap max-w-[1000px] transition-all duration-500"
v-if="loadStatus == 1">
<!-- 资源项 -->
<a class="w-full xs:w-[380px] lg:w-[420px] sm:mb-[40px] mb-[20px] mx-0 xs:mx-[30px] p-[30px] bg-white dark:bg-transparent dark:border-[1px] dark:border-[#aaa] rounded-0 xs:rounded-[15px] flex justify-between shadow-xl hover:shadow-2xl dark:hover:shadow-[#666] transition-all duration-500 overflow-hidden"
v-for="item of ResourceDatas" :href="item.src" target="_blank">
<!-- 左侧图标 -->
<div
class="w-[80px] h-[80px] overflow-hidden shadow-md rounded-[10px] transition-all duration-300 dark:border-[1px]">
<img :src="item.icon_src" alt="" class="w-[80px] h-[80px]">
</div>
<!-- 右侧描述 -->
<div class="ml-[20px] flex-1 cursor-default">
<div class="text-[23px] font-semibold text-[#333] dark:text-white ">{{ item.title }}</div>
<div class="text-[14px] mt-[5px] text-[#666] dark:text-[#ccc]">{{ item.describe }}</div>
<!-- 下方的标签 -->
<div class="flex gap-[5px] mt-[10px] flex-wrap">
<div
class="text-[10px] text-[#666] dark:text-[#ccc] py-[1px] px-[6px] rounded-[20px] dark:bg-transparent dark:border-[1px] dark:border-[#999]"
:class="[{ '!bg-[#d6eeff] dark:!bg-[#ffffff22]': lable.type === 'OS' }, { 'bg-[#eceef1]': lable.type !== 'OS' }]"
v-for="lable of item.addition.lables">{{
lable.text }}</div>
</div>
</div>
<!-- 右上方的标签 -->
<div class="relative w-0 h-0 bottom-[18px] right-[38px] text-center" v-if="item.addition.lable.text">
<div class="w-[100px] h-[18px] bg-[#ff1900b3] text-white text-[12px] font-semibold rotate-[45deg]"
:class="{ 'bg-[#ff8000b2]': (item.addition.lable.class.indexOf('lable-2') != -1) }">{{
item.addition.lable.text }}</div>
</div>
</a>
<div class="xs:w-[380px] w-0 lg:w-[420px] mx-[30px] transition-all duration-300"></div>
</div>
</div>
<Agreement v-if="showAgreement" @closeAgreement="showAgreement = false" />
</template>

View File

@@ -1,61 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
onMounted(() => {
window.document.title = `特恩的日志`;
// 界面特效字体
let nameElement = document.getElementById("my-name");
if (nameElement == null) {
return console.error('未找到元素my-name')
}
let colorNum = 66;
let colorNumReverse = false;
setInterval(() => {
if (colorNumReverse) {
colorNum--;
if (colorNum <= 66)
colorNumReverse = !colorNumReverse;
} else {
colorNum++;
if (colorNum >= 255)
colorNumReverse = !colorNumReverse;
}
nameElement.style.backgroundImage = `linear-gradient(45deg, rgb(${colorNum}, 66, ${255 - (66 - colorNum)}), rgb(${255 - (66 - colorNum)}, 66, ${colorNum}))`;
}, 20);
})
</script>
<template>
<div class="bg-default-bg fixed inset-0 w-full h-full -z-10 dark:bg-[#222]"></div>
<div
class="flex max-w-[800px] flex-col items-center justify-center mx-auto min-h-[420px] sm:h-[calc(100vh-90px)] h-[calc(100vh-77px)]"
id="home-main">
<img src="../assets/logo.jpg" alt="" class="w-[150px] rounded-full">
<div class="name text-[34px] sm:text-[42px]" id="my-name">特恩(TONE)</div>
<div
class="mt-[10px] sm:text-[23px] text-[16px] cursor-default text-[#888] transition-all duration-200 dark:text-[#ccc]">
一名计算机类专业在校本科大三学生</div>
<div class="mt-[28px] mb-[30px] flex items-center justify-center flex-wrap sm:flex-row flex-col gap-[10px]">
<a href="https://space.bilibili.com/474156211" target="_blank">
<button
class="cursor-pointer sm:w-[130px] w-[180px] sm:h-[46px] h-[40px] bg-[#2591f0] hover:bg-[#1d74c0] text-white border-none text-[16px] rounded-[23px] mx-[10px] transition-all duration-200 shadow-lg"
id="button-resource">哔哩哔哩</button>
</a>
<a href="https://github.com/tonecn" target="_blank">
<button
class="cursor-pointer sm:w-[130px] w-[180px] sm:h-[46px] h-[40px] bg-[#e87f29] hover:bg-[#d5782c] text-white border-none text-[16px] rounded-[23px] mx-[10px] transition-all duration-200 shadow-lg"
id="button-code">GitHub</button>
</a>
</div>
</div>
</template>
<style scoped>
.name {
font-weight: 800;
margin-top: 25px;
cursor: default;
background: linear-gradient(45deg, rgb(66, 66, 255), rgb(255, 66, 66));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
transition: font-size 0.2s;
}
</style>

View File

@@ -1,18 +0,0 @@
<script setup lang='ts'>
import { onMounted, onUnmounted } from 'vue';
onMounted(async () => {
document.title = '404 不存在的页面';
})
onUnmounted(async () => {
document.title = '特恩(TONE)'
})
</script>
<template>
<div class="bg-default-bg fixed inset-0 w-full h-full -z-10 dark:bg-[#222]"></div>
<div
class="flex max-w-[800px] flex-col items-center justify-center mx-auto min-h-[320px] sm:h-[calc(100vh-90px)] h-[calc(100vh-77px)]">
<el-empty id="404-notfound-el-empty" style="margin-bottom: 100px;" description="404 不存在的页面" />
</div>
</template>

View File

@@ -1,75 +0,0 @@
<script setup lang="ts">
import { request, type BaseResponseData } from '@/lib/request';
import Agreement from '@/components/Common/Agreement.vue';
import { ref, onMounted, reactive } from 'vue';
let showAgreement = ref(false);
let loadStatus = ref(0);// 0加载中 1加载成功 2加载失败
let ResourceDatas: any[] = reactive([])
onMounted(async () => {
// 用于获取数据的函数
try {
let res: BaseResponseData = await request.get('/resourceList?type=resource');
if (res && res.code == 0) {
loadStatus.value = 1;
ResourceDatas.push(...res.data)
} else {
throw new Error(res.message)
}
} catch (error) {
console.error(error)
loadStatus.value = 2;
}
})
</script>
<template>
<div class="mx-auto flex flex-col items-center">
<div class="bg-default-bg fixed inset-0 w-full h-full -z-10 dark:bg-[#222]"></div>
<div
class="mt-[30px] sm:mt-[80px] text-[30px] sm:text-[42px] cursor-default transition-all duration-500 dark:text-[#ddd] text-[#333]">
精心挑选并收藏的资源
</div>
<div class="mt-[20px] text-[#666] cursor-default text-[12px] px-[20px] dark:text-[#ccc]" v-if="loadStatus != 2">
请在浏览此部分内容前阅读并同意
<a class="text-[#222] cursor-pointer underline] dark:text-[#fff]"
@click="showAgreement = true">网站使用协议</a>继续使用或浏览表示您接受协议条款
</div>
<div class="mt-[80px] cursor-default mb-[380px] text-center dark:text-[#ddd]" v-if="loadStatus == 2">加载失败请刷新界面重试
</div>
<div class="mt-[80px] cursor-default mb-[380px] text-center dark:text-[#ddd]" v-if="loadStatus == 0">加载中请稍后...
</div>
<div
class="mt-[25px] sm:mt-[50px] mx-auto flex justify-center items-start flex-wrap max-w-[1000px] transition-all duration-500"
v-if="loadStatus == 1">
<!-- 资源项 -->
<a class="w-full xs:w-[380px] lg:w-[420px] sm:mb-[40px] mb-[20px] mx-0 xs:mx-[30px] p-[30px] bg-white dark:bg-transparent dark:border-[1px] dark:border-[#aaa] rounded-0 xs:rounded-[15px] flex justify-between shadow-xl hover:shadow-2xl dark:hover:shadow-[#666] transition-all duration-500 overflow-hidden"
v-for="item of ResourceDatas" :href="item.src" target="_blank">
<!-- 左侧图标 -->
<div
class="w-[80px] h-[80px] overflow-hidden shadow-md rounded-[10px] transition-all duration-300 dark:border-[1px]">
<img :src="item.icon_src" alt="" class="w-[80px] h-[80px]">
</div>
<!-- 右侧描述 -->
<div class="ml-[20px] flex-1 cursor-default">
<div class="text-[23px] font-semibold text-[#333] dark:text-white ">{{ item.title }}</div>
<div class="text-[14px] mt-[5px] text-[#666] dark:text-[#ccc]">{{ item.describe }}</div>
<!-- 下方的标签 -->
<div class="flex gap-[5px] mt-[10px] flex-wrap">
<div
class="text-[10px] text-[#666] dark:text-[#ccc] py-[1px] px-[6px] rounded-[20px] dark:bg-transparent dark:border-[1px] dark:border-[#999]"
:class="[{ '!bg-[#d6eeff] dark:!bg-[#ffffff22]': lable.type === 'OS' }, { 'bg-[#eceef1]': lable.type !== 'OS' }]"
v-for="lable of item.addition.lables">{{
lable.text }}</div>
</div>
</div>
<!-- 右上方的标签 -->
<div class="relative w-0 h-0 bottom-[18px] right-[38px] text-center" v-if="item.addition.lable.text">
<div class="w-[100px] h-[18px] bg-[#ff1900b3] text-white text-[12px] font-semibold rotate-[45deg]"
:class="{ 'bg-[#ff8000b2]': (item.addition.lable.class.indexOf('lable-2') != -1) }">{{
item.addition.lable.text }}</div>
</div>
</a>
<div class="xs:w-[380px] w-0 lg:w-[420px] mx-[30px] transition-all duration-300"></div>
</div>
</div>
<Agreement v-if="showAgreement" @closeAgreement="showAgreement = false" />
</template>

View File

@@ -1,20 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./src/**/*.{vue,js,ts,jsx,tsx}",
"./index.html",
],
theme: {
extend: {
colors: {
'default-bg': '#fafafa',
'dark-bg': '#1a1a1a',
},
screens: {
'xs': '440px'
}
},
},
plugins: [],
}

View File

@@ -1,17 +0,0 @@
{
"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/*"],
"@components/*": ["./src/components/*"],
"@views/*": ["./src/views/*"],
"@assets/*": ["./src/assets/*"],
}
}
}

View File

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

View File

@@ -1,19 +0,0 @@
{
"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"]
}
}

View File

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