20 Commits

Author SHA1 Message Date
jiangrui
32bf77c3e0 chore(release): bump version to v0.1.2 2025-03-02 08:33:12 +08:00
jiangrui
9f6b811785 Merge remote-tracking branch 'origin/dev' 2025-03-02 08:29:15 +08:00
jiangrui
fe38437a7c update:群二维码 2025-03-01 08:37:51 +08:00
jiangrui
06b01bf1f9 Merge remote-tracking branch 'origin/main' into dev 2025-02-27 14:28:32 +08:00
jiangrui1994
2fffdc30f5 Update docker-build-test.yml 2025-02-26 16:10:08 +08:00
jiangrui
5b701e499a fix:转存报错没有提示 2025-02-25 13:24:01 +08:00
jiangrui
25ecdb1078 Merge remote-tracking branch 'origin/main' into dev 2025-02-25 13:04:03 +08:00
jiangrui1994
1c884eb3b9 Update docker-build-test.yml 2025-02-25 13:03:52 +08:00
jiangrui
e4d2a1db52 Merge remote-tracking branch 'origin/main' into dev 2025-02-25 13:02:28 +08:00
jiangrui1994
4553b9a7a0 Create docker-build-test.yml 2025-02-25 13:01:33 +08:00
jiangrui
c290f5a6d9 feat:增加转存资源列表展示与选择 2025-02-25 12:53:27 +08:00
jiangrui
8668bce863 fix:修复部分115账号cookie无法使用问题 2025-02-25 12:51:34 +08:00
jiangrui1994
390287876a Merge pull request #1 from jiangrui1994/dev
Dev
2025-02-25 09:37:54 +08:00
jiangrui
f860cc1f57 refactor: 重构图片代理控制器,优化代码结构 2025-02-24 16:10:46 +08:00
jiangrui
f5106e782a format:format and fix code 2025-02-24 15:22:43 +08:00
jiangrui
c784b562c4 fix:修复夸克空文件夹选中报错 2025-02-21 15:02:27 +08:00
jiangrui
cd4fa8cc2b Update README.md 2025-02-21 10:56:43 +08:00
jiangrui1994
2041a7fa11 Update README.md 2025-02-21 09:55:31 +08:00
jiangrui1994
924903bd58 Update docker-image.yml 2025-02-21 09:51:02 +08:00
jiangrui1994
e3bf2d6d6b Update README.md 2025-02-20 17:49:27 +08:00
52 changed files with 2359 additions and 1223 deletions

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
build
coverage

57
.eslintrc.js Normal file
View File

@@ -0,0 +1,57 @@
module.exports = {
root: true,
ignorePatterns: ["node_modules", "dist", "build", "coverage"],
env: {
node: true,
es6: true,
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/explicit-function-return-type": 0,
},
overrides: [
{
files: ["frontend/**/*.{js,ts,vue}"],
env: {
browser: true,
},
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended",
"plugin:prettier/recommended",
],
plugins: ["@typescript-eslint", "vue"],
rules: {
"vue/multi-word-component-names": "off",
"vue/require-default-prop": "off",
"vue/no-v-html": "off",
},
},
{
files: ["backend/**/*.{js,ts}"],
env: {
node: true,
},
rules: {
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-non-null-assertion": "warn",
},
},
],
};

44
.github/workflows/docker-build-test.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Build and Push Multi-Arch Docker Image for Test
on:
push:
branches:
- dev
workflow_dispatch: # 添加手动触发
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # 必须授权以推送镜像
env:
REPO_NAME: ${{ github.repository }}
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置小写镜像名称
run: |
LOWER_NAME=$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')
echo "LOWER_NAME=$LOWER_NAME" >> $GITHUB_ENV
- name: 登录到 GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 设置 QEMU 支持多架构
uses: docker/setup-qemu-action@v2
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 构建并推送多架构 Docker 镜像
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64 # 指定架构x86_64 和 ARM64
push: true
tags: |
ghcr.io/${{ env.LOWER_NAME }}:test

View File

@@ -1,7 +1,6 @@
name: Docker Image CI/CD name: Docker Image CI/CD
on: on:
push: push:
branches: [ "main" ]
tags: [ "v*.*.*" ] # 支持标签触发(如 v1.0.0 tags: [ "v*.*.*" ] # 支持标签触发(如 v1.0.0
jobs: jobs:
build-and-push: build-and-push:

View File

@@ -25,3 +25,6 @@ node_modules
# 系统文件 # 系统文件
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# 版本控制
.git

14
.prettierrc.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
semi: true,
trailingComma: "es5",
singleQuote: false,
printWidth: 100,
tabWidth: 2,
useTabs: false,
endOfLine: "auto",
arrowParens: "always",
bracketSpacing: true,
embeddedLanguageFormatting: "auto",
htmlWhitespaceSensitivity: "css",
vueIndentScriptAndStyle: false,
};

View File

@@ -1,11 +0,0 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"printWidth": 100,
"trailingComma": "es5",
"bracketSpacing": true,
"endOfLine": "lf",
"arrowParens": "always",
"vueIndentScriptAndStyle": true
}

View File

@@ -12,8 +12,8 @@
## 注意事项 ## 注意事项
1. 此项目的资源搜索需要用到代理环境,请自行搭建。 1. 此项目的资源搜索需要用到**代理环境**,请自行搭建。
2. 新用户注册管理员默认注册码230713普通用户默认注册码9527 2. 新用户注册,**管理员**默认注册码230713普通用户默认注册码9527
## 功能特性 ## 功能特性
@@ -44,18 +44,14 @@
<img src="./docs/images/hotmovie.png" width="400"> <img src="./docs/images/hotmovie.png" width="400">
### 转存 ### 转存
<img src="./docs/images/save.png" width="400"> <img src="./docs/images/save.png" width="400">
### 系统设置 ### 系统设置
<img src="./docs/images/setting.png" width="400"> <img src="./docs/images/setting.png" width="400">
## 技术栈 ## 技术栈
### 前端 ### 前端
@@ -109,20 +105,22 @@ cp .env.example ./backend/.env
## Docker 部署 ## Docker 部署
- 构建镜像:
```bash
# 构建示例
docker build --platform linux/amd64 -t cloud-saver . --no-cache
```
- 运行容器: - 运行容器:
```bash ```bash
docker run -d -p 8008:8008 --name cloud-saver cloud-saver docker run -d -p 8008:8008 --name cloud-saver ghcr.io/jiangrui1994/cloudsaver
``` ```
### docker 镜像地址
[ghcr.io/jiangrui1994/cloudsaver](ghcr.io/jiangrui1994/cloudsaver)
相关nas镜像拉取 可从此地址拉取
## 交流沟通 ## 交流沟通
<img src="./docs/images/wechat.jpg" width="400">
<img src="./docs/images/20250220115710.jpg" width="400"> <img src="./docs/images/20250220115710.jpg" width="400">
<img src="./docs/images/20241217122628.jpg" width="400">
## License ## License

View File

@@ -38,7 +38,7 @@ app.use((req, res, next) => {
app.use("/", routes); app.use("/", routes);
const initializeGlobalSettings = async () => { const initializeGlobalSettings = async (): Promise<void> => {
const settings = await GlobalSetting.findOne(); const settings = await GlobalSetting.findOne();
if (!settings) { if (!settings) {
await GlobalSetting.create({ await GlobalSetting.create({

View File

@@ -1,15 +1,14 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response } from "express";
import { Cloud115Service } from "../services/Cloud115Service"; import { Cloud115Service } from "../services/Cloud115Service";
import { sendSuccess, sendError } from "../utils/response"; import { sendSuccess, sendError } from "../utils/response";
import UserSetting from "../models/UserSetting"; import UserSetting from "../models/UserSetting";
const cloud115 = new Cloud115Service(); const cloud115 = new Cloud115Service();
const setCookie = async (req: Request) => { const setCookie = async (req: Request): Promise<void> => {
const userId = req.user?.userId; const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({ const userSetting = await UserSetting.findOne({
where: { userId }, where: { userId },
}); });
console.log(userSetting?.dataValues.cloud115Cookie);
if (userSetting && userSetting.dataValues.cloud115Cookie) { if (userSetting && userSetting.dataValues.cloud115Cookie) {
cloud115.setCookie(userSetting.dataValues.cloud115Cookie); cloud115.setCookie(userSetting.dataValues.cloud115Cookie);
} else { } else {
@@ -18,7 +17,7 @@ const setCookie = async (req: Request) => {
}; };
export const cloud115Controller = { export const cloud115Controller = {
async getShareInfo(req: Request, res: Response, next: NextFunction) { async getShareInfo(req: Request, res: Response): Promise<void> {
try { try {
const { shareCode, receiveCode } = req.query; const { shareCode, receiveCode } = req.query;
await setCookie(req); await setCookie(req);
@@ -30,7 +29,7 @@ export const cloud115Controller = {
} }
}, },
async getFolderList(req: Request, res: Response, next: NextFunction) { async getFolderList(req: Request, res: Response): Promise<void> {
try { try {
const { parentCid } = req.query; const { parentCid } = req.query;
await setCookie(req); await setCookie(req);
@@ -41,7 +40,7 @@ export const cloud115Controller = {
} }
}, },
async saveFile(req: Request, res: Response, next: NextFunction) { async saveFile(req: Request, res: Response): Promise<void> {
try { try {
const { shareCode, receiveCode, fileId, folderId } = req.body; const { shareCode, receiveCode, fileId, folderId } = req.body;
await setCookie(req); await setCookie(req);

View File

@@ -1,11 +1,11 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response } from "express";
import DoubanService from "../services/DoubanService"; import DoubanService from "../services/DoubanService";
import { sendSuccess, sendError } from "../utils/response"; import { sendSuccess, sendError } from "../utils/response";
const doubanService = new DoubanService(); const doubanService = new DoubanService();
export const doubanController = { export const doubanController = {
async getDoubanHotList(req: Request, res: Response, next: NextFunction) { async getDoubanHotList(req: Request, res: Response): Promise<void> {
try { try {
const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query; const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query;
const result = await doubanService.getHotList({ const result = await doubanService.getHotList({

View File

@@ -1,11 +1,11 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response } from "express";
import { QuarkService } from "../services/QuarkService"; import { QuarkService } from "../services/QuarkService";
import { sendSuccess, sendError } from "../utils/response"; import { sendSuccess, sendError } from "../utils/response";
import UserSetting from "../models/UserSetting"; import UserSetting from "../models/UserSetting";
const quark = new QuarkService(); const quark = new QuarkService();
const setCookie = async (req: Request) => { const setCookie = async (req: Request): Promise<void> => {
const userId = req.user?.userId; const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({ const userSetting = await UserSetting.findOne({
where: { userId }, where: { userId },
@@ -18,7 +18,7 @@ const setCookie = async (req: Request) => {
}; };
export const quarkController = { export const quarkController = {
async getShareInfo(req: Request, res: Response, next: NextFunction) { async getShareInfo(req: Request, res: Response): Promise<void> {
try { try {
const { pwdId, passcode } = req.query; const { pwdId, passcode } = req.query;
await setCookie(req); await setCookie(req);
@@ -29,7 +29,7 @@ export const quarkController = {
} }
}, },
async getFolderList(req: Request, res: Response, next: NextFunction) { async getFolderList(req: Request, res: Response): Promise<void> {
try { try {
const { parentCid } = req.query; const { parentCid } = req.query;
await setCookie(req); await setCookie(req);
@@ -40,7 +40,7 @@ export const quarkController = {
} }
}, },
async saveFile(req: Request, res: Response, next: NextFunction) { async saveFile(req: Request, res: Response): Promise<void> {
try { try {
await setCookie(req); await setCookie(req);
const result = await quark.saveSharedFile(req.body); const result = await quark.saveSharedFile(req.body);

View File

@@ -1,22 +1,9 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response } from "express";
import { RSSSearcher } from "../services/RSSSearcher";
import Searcher from "../services/Searcher"; import Searcher from "../services/Searcher";
import { sendSuccess, sendError } from "../utils/response"; import { sendSuccess, sendError } from "../utils/response";
export const resourceController = { export const resourceController = {
async rssSearch(req: Request, res: Response, next: NextFunction) { async search(req: Request, res: Response): Promise<void> {
try {
const { keyword } = req.query;
const searcher = new RSSSearcher();
const result = await searcher.searchAll(keyword as string);
sendSuccess(res, result);
} catch (error) {
sendError(res, {
message: (error as Error).message || "RSS 搜索失败",
});
}
},
async search(req: Request, res: Response, next: NextFunction) {
try { try {
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
const result = await Searcher.searchAll( const result = await Searcher.searchAll(

View File

@@ -1,11 +1,12 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response } from "express";
import { sendSuccess, sendError } from "../utils/response"; import { sendSuccess, sendError } from "../utils/response";
import Searcher from "../services/Searcher"; import Searcher from "../services/Searcher";
import UserSetting from "../models/UserSetting"; import UserSetting from "../models/UserSetting";
import GlobalSetting from "../models/GlobalSetting"; import GlobalSetting from "../models/GlobalSetting";
import { iamgesInstance } from "./teleImages";
export const settingController = { export const settingController = {
async get(req: Request, res: Response) { async get(req: Request, res: Response): Promise<void> {
try { try {
const userId = req.user?.userId; const userId = req.user?.userId;
const role = req.user?.role; const role = req.user?.role;
@@ -36,7 +37,7 @@ export const settingController = {
sendError(res, { message: (error as Error).message || "获取设置失败" }); sendError(res, { message: (error as Error).message || "获取设置失败" });
} }
}, },
async save(req: Request, res: Response) { async save(req: Request, res: Response): Promise<void> {
try { try {
const userId = req.user?.userId; const userId = req.user?.userId;
const role = req.user?.role; const role = req.user?.role;
@@ -45,6 +46,7 @@ export const settingController = {
await UserSetting.update(userSettings, { where: { userId } }); await UserSetting.update(userSettings, { where: { userId } });
if (role === 1 && globalSetting) await GlobalSetting.update(globalSetting, { where: {} }); if (role === 1 && globalSetting) await GlobalSetting.update(globalSetting, { where: {} });
Searcher.updateAxiosInstance(); Searcher.updateAxiosInstance();
iamgesInstance.updateProxyConfig();
sendSuccess(res, { sendSuccess(res, {
message: "保存成功", message: "保存成功",
}); });

View File

@@ -1,43 +1,64 @@
import axios, { AxiosInstance } from "axios"; import axios, { AxiosInstance } from "axios";
import { Request, Response } from "express"; import e, { Request, Response } from "express";
import tunnel from "tunnel"; import tunnel from "tunnel";
import GlobalSetting from "../models/GlobalSetting"; import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting"; import { GlobalSettingAttributes } from "../models/GlobalSetting";
export class ImageControll { export class ImageControll {
private axiosInstance: AxiosInstance | null = null; private axiosInstance: AxiosInstance | null = null;
private isUpdate = false; private settings: GlobalSetting | null = null;
constructor() { constructor() {
this.initializeAxiosInstance(); this.initializeAxiosInstance();
} }
private async initializeAxiosInstance(isUpdate = false) { private async initializeAxiosInstance(): Promise<void> {
let settings = null; try {
if (isUpdate) { this.settings = await GlobalSetting.findOne();
settings = await GlobalSetting.findOne(); } catch (error) {
this.isUpdate = isUpdate; console.error("Error fetching global settings:", error);
} else {
return;
} }
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes); const globalSetting = this.settings?.dataValues || ({} as GlobalSettingAttributes);
this.axiosInstance = axios.create({ this.axiosInstance = axios.create({
timeout: 3000, timeout: 3000,
httpsAgent: tunnel.httpsOverHttp({ httpsAgent: globalSetting.isProxyEnabled
proxy: { ? tunnel.httpsOverHttp({
host: globalSetting.httpProxyHost, proxy: {
port: globalSetting.httpProxyPort, host: globalSetting.httpProxyHost,
headers: { port: globalSetting.httpProxyPort,
"Proxy-Authorization": "", headers: {
}, "Proxy-Authorization": "",
}, },
}), },
})
: undefined,
withCredentials: true, withCredentials: true,
}); });
} }
async getImages(req: Request, res: Response, url: string) { public async updateProxyConfig(): Promise<void> {
try {
this.settings = await GlobalSetting.findOne();
const globalSetting = this.settings?.dataValues || ({} as GlobalSettingAttributes);
if (this.axiosInstance) {
this.axiosInstance.defaults.httpsAgent = globalSetting.isProxyEnabled
? tunnel.httpsOverHttp({
proxy: {
host: globalSetting.httpProxyHost,
port: globalSetting.httpProxyPort,
headers: {
"Proxy-Authorization": "",
},
},
})
: undefined;
}
} catch (error) {
console.error("Error updating proxy config:", error);
}
}
async getImages(req: Request, res: Response, url: string): Promise<void> {
try { try {
if (!this.isUpdate) await this.initializeAxiosInstance(true);
const response = await this.axiosInstance?.get(url, { responseType: "stream" }); const response = await this.axiosInstance?.get(url, { responseType: "stream" });
res.set("Content-Type", response?.headers["content-type"]); res.set("Content-Type", response?.headers["content-type"]);
response?.data.pipe(res); response?.data.pipe(res);
@@ -47,10 +68,10 @@ export class ImageControll {
} }
} }
const iamgesInstance = new ImageControll(); export const iamgesInstance = new ImageControll();
export const imageControll = { export const imageControll = {
getImages: async (req: Request, res: Response) => { getImages: async (req: Request, res: Response): Promise<void> => {
const url = req.query.url as string; const url = req.query.url as string;
iamgesInstance.getImages(req, res, url); iamgesInstance.getImages(req, res, url);
}, },

View File

@@ -12,7 +12,7 @@ const isValidInput = (input: string): boolean => {
return regex.test(input); return regex.test(input);
}; };
export const userController = { export const userController = {
async register(req: Request, res: Response) { async register(req: Request, res: Response): Promise<void> {
const { username, password, registerCode } = req.body; const { username, password, registerCode } = req.body;
const globalSetting = await GlobalSetting.findOne(); const globalSetting = await GlobalSetting.findOne();
const registerCodeList = [ const registerCodeList = [
@@ -39,12 +39,12 @@ export const userController = {
data: user, data: user,
message: "用户注册成功", message: "用户注册成功",
}); });
} catch (error: any) { } catch (error) {
sendError(res, { message: error.message || "用户注册失败" }); sendError(res, { message: (error as Error).message || "用户注册失败" });
} }
}, },
async login(req: Request, res: Response) { async login(req: Request, res: Response): Promise<void> {
const { username, password } = req.body; const { username, password } = req.body;
const user = await User.findOne({ where: { username } }); const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.password))) { if (!user || !(await bcrypt.compare(password, user.password))) {

View File

@@ -15,7 +15,7 @@ export const authMiddleware = async (
req: AuthenticatedRequest, req: AuthenticatedRequest,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ): Promise<void | Response> => {
if (req.path === "/user/login" || req.path === "/user/register") { if (req.path === "/user/login" || req.path === "/user/register") {
return next(); return next();
} }

View File

@@ -1,6 +1,10 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response } from "express";
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => { interface CustomError extends Error {
status?: number;
}
export const errorHandler = (err: CustomError, req: Request, res: Response): void => {
console.error(err); console.error(err);
res.status(err.status || 500).json({ res.status(err.status || 500).json({
success: false, success: false,

View File

@@ -1,6 +1,8 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
export const validateRequest = (requiredParams: string[]) => { export const validateRequest = (
requiredParams: string[]
): ((req: Request, res: Response, next: NextFunction) => Response | void) => {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]); const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]);
if (missingParams.length > 0) { if (missingParams.length > 0) {

View File

@@ -19,7 +19,6 @@ router.use("/setting", settingRoutes);
// 资源搜索 // 资源搜索
router.get("/search", resourceController.search); router.get("/search", resourceController.search);
router.get("/rssSearch", resourceController.rssSearch);
// 115网盘相关 // 115网盘相关
router.get("/cloud115/share-info", cloud115Controller.getShareInfo); router.get("/cloud115/share-info", cloud115Controller.getShareInfo);

View File

@@ -4,6 +4,23 @@ import { Logger } from "../utils/logger";
import { config } from "../config/index"; import { config } from "../config/index";
import { ShareInfoResponse } from "../types/cloud115"; import { ShareInfoResponse } from "../types/cloud115";
interface Cloud115ListItem {
cid: string;
n: string;
s: number;
}
interface Cloud115FolderItem {
cid: string;
n: string;
ns: number;
}
interface Cloud115PathItem {
cid: string;
name: string;
}
export class Cloud115Service { export class Cloud115Service {
private api: AxiosInstance; private api: AxiosInstance;
private cookie: string = ""; private cookie: string = "";
@@ -39,7 +56,7 @@ export class Cloud115Service {
}); });
} }
public setCookie(cookie: string) { public setCookie(cookie: string): void {
this.cookie = cookie; this.cookie = cookie;
} }
@@ -53,10 +70,9 @@ export class Cloud115Service {
cid: "", cid: "",
}, },
}); });
if (response.data?.state && response.data.data?.list?.length > 0) { if (response.data?.state && response.data.data?.list?.length > 0) {
return { return {
data: response.data.data.list.map((item: any) => ({ data: response.data.data.list.map((item: Cloud115ListItem) => ({
fileId: item.cid, fileId: item.cid,
fileName: item.n, fileName: item.n,
fileSize: item.s, fileSize: item.s,
@@ -66,13 +82,15 @@ export class Cloud115Service {
throw new Error("未找到文件信息"); throw new Error("未找到文件信息");
} }
async getFolderList(parentCid = "0") { async getFolderList(
parentCid = "0"
): Promise<{ data: { cid: string; name: string; path: Cloud115PathItem[] }[] }> {
const response = await this.api.get("/files", { const response = await this.api.get("/files", {
params: { params: {
aid: 1, aid: 1,
cid: parentCid, cid: parentCid,
o: "user_ptime", o: "user_ptime",
asc: 0, asc: 1,
offset: 0, offset: 0,
show_dir: 1, show_dir: 1,
limit: 50, limit: 50,
@@ -80,15 +98,17 @@ export class Cloud115Service {
format: "json", format: "json",
star: 0, star: 0,
suffix: "", suffix: "",
natsort: 1, natsort: 0,
snap: 0,
record_open_time: 1,
fc_mix: 0,
}, },
}); });
if (response.data?.state) { if (response.data?.state) {
return { return {
data: response.data.data data: response.data.data
.filter((item: any) => item.cid && !!item.ns) .filter((item: Cloud115FolderItem) => item.cid && !!item.ns)
.map((folder: any) => ({ .map((folder: Cloud115FolderItem) => ({
cid: folder.cid, cid: folder.cid,
name: folder.n, name: folder.n,
path: response.data.path, path: response.data.path,
@@ -105,7 +125,7 @@ export class Cloud115Service {
shareCode: string; shareCode: string;
receiveCode: string; receiveCode: string;
fileId: string; fileId: string;
}) { }): Promise<{ message: string; data: unknown }> {
const param = new URLSearchParams({ const param = new URLSearchParams({
cid: params.cid, cid: params.cid,
user_id: config.cloud115.userId, user_id: config.cloud115.userId,

View File

@@ -1,6 +1,15 @@
import { AxiosHeaders, AxiosInstance } from "axios"; import { AxiosHeaders, AxiosInstance } from "axios";
import { createAxiosInstance } from "../utils/axiosInstance"; import { createAxiosInstance } from "../utils/axiosInstance";
interface DoubanSubject {
id: string;
title: string;
rate: string;
cover: string;
url: string;
is_new: boolean;
}
class DoubanService { class DoubanService {
private baseUrl: string; private baseUrl: string;
private api: AxiosInstance; private api: AxiosInstance;
@@ -28,7 +37,12 @@ class DoubanService {
); );
} }
async getHotList(params: { type: string; tag: string; page_limit: string; page_start: string }) { async getHotList(params: {
type: string;
tag: string;
page_limit: string;
page_start: string;
}): Promise<{ data: DoubanSubject[] }> {
try { try {
const response = await this.api.get("/search_subjects", { const response = await this.api.get("/search_subjects", {
params: params, params: params,

View File

@@ -2,6 +2,24 @@ import { AxiosInstance, AxiosHeaders } from "axios";
import { Logger } from "../utils/logger"; import { Logger } from "../utils/logger";
import { createAxiosInstance } from "../utils/axiosInstance"; import { createAxiosInstance } from "../utils/axiosInstance";
interface QuarkShareInfo {
stoken?: string;
pwdId?: string;
fileSize?: number;
list: {
fid: string;
file_name: string;
file_type: number;
share_fid_token: string;
}[];
}
interface QuarkFolderItem {
fid: string;
file_name: string;
file_type: number;
}
export class QuarkService { export class QuarkService {
private api: AxiosInstance; private api: AxiosInstance;
private cookie: string = ""; private cookie: string = "";
@@ -34,11 +52,11 @@ export class QuarkService {
}); });
} }
public setCookie(cookie: string) { public setCookie(cookie: string): void {
this.cookie = cookie; this.cookie = cookie;
} }
async getShareInfo(pwdId: string, passcode = "") { async getShareInfo(pwdId: string, passcode = ""): Promise<{ data: QuarkShareInfo }> {
const response = await this.api.post( const response = await this.api.post(
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`, `/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
{ {
@@ -49,7 +67,7 @@ export class QuarkService {
if (response.data?.status === 200 && response.data.data) { if (response.data?.status === 200 && response.data.data) {
const fileInfo = response.data.data; const fileInfo = response.data.data;
if (fileInfo.stoken) { if (fileInfo.stoken) {
let res = await this.getShareList(pwdId, fileInfo.stoken); const res = await this.getShareList(pwdId, fileInfo.stoken);
return { return {
data: res, data: res,
}; };
@@ -58,7 +76,7 @@ export class QuarkService {
throw new Error("获取夸克分享信息失败"); throw new Error("获取夸克分享信息失败");
} }
async getShareList(pwdId: string, stoken: string) { async getShareList(pwdId: string, stoken: string): Promise<QuarkShareInfo> {
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", { const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
params: { params: {
pr: "ucpro", pr: "ucpro",
@@ -80,8 +98,8 @@ export class QuarkService {
}); });
if (response.data?.data) { if (response.data?.data) {
const list = response.data.data.list const list = response.data.data.list
.filter((item: any) => item.fid) .filter((item: QuarkShareInfo["list"][0]) => item.fid)
.map((folder: any) => ({ .map((folder: QuarkShareInfo["list"][0]) => ({
fileId: folder.fid, fileId: folder.fid,
fileName: folder.file_name, fileName: folder.file_name,
fileIdToken: folder.share_fid_token, fileIdToken: folder.share_fid_token,
@@ -89,7 +107,8 @@ export class QuarkService {
return { return {
list, list,
pwdId, pwdId,
stoken: stoken, stoken,
fileSize: response.data.data.share?.size || 0,
}; };
} else { } else {
return { return {
@@ -98,7 +117,9 @@ export class QuarkService {
} }
} }
async getFolderList(parentCid = "0") { async getFolderList(
parentCid = "0"
): Promise<{ data: { cid: string; name: string; path: [] }[] }> {
const response = await this.api.get("/1/clouddrive/file/sort", { const response = await this.api.get("/1/clouddrive/file/sort", {
params: { params: {
pr: "ucpro", pr: "ucpro",
@@ -114,10 +135,10 @@ export class QuarkService {
__t: Date.now(), __t: Date.now(),
}, },
}); });
if (response.data?.data && response.data.data.list.length) { if (response.data?.data && response.data.data.list) {
const data = response.data.data.list const data = response.data.data.list
.filter((item: any) => item.fid && item.file_type === 0) .filter((item: QuarkFolderItem) => item.fid && item.file_type === 0)
.map((folder: any) => ({ .map((folder: QuarkFolderItem) => ({
cid: folder.fid, cid: folder.fid,
name: folder.file_name, name: folder.file_name,
path: [], path: [],
@@ -140,7 +161,7 @@ export class QuarkService {
stoken: string; stoken: string;
pdir_fid: string; pdir_fid: string;
scene: string; scene: string;
}) { }): Promise<{ message: string; data: unknown }> {
try { try {
const response = await this.api.post( const response = await this.api.post(
`/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`, `/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`,

View File

@@ -1,116 +0,0 @@
import RSSParser from "rss-parser";
import { AxiosInstance, AxiosHeaders } from "axios";
import { config } from "../config";
import { Logger } from "../utils/logger";
import { createAxiosInstance } from "../utils/axiosInstance";
import { data } from "cheerio/dist/commonjs/api/attributes";
interface RSSItem {
title?: string;
link?: string;
pubDate?: string;
content?: string;
description?: string;
image?: string;
cloudLinks?: string[];
}
export class RSSSearcher {
private parser: RSSParser;
private axiosInstance: AxiosInstance;
constructor() {
this.parser = new RSSParser({
customFields: {
item: [
["content:encoded", "content"],
["description", "description"],
],
},
});
this.axiosInstance = createAxiosInstance(
config.rss.baseUrl,
AxiosHeaders.from({
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: "application/xml,application/xhtml+xml,text/html,application/rss+xml",
}),
true
);
}
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
const links: string[] = [];
let cloudType = "";
Object.values(config.cloudPatterns).forEach((pattern, index) => {
const matches = text.match(pattern);
if (matches) {
links.push(...matches);
cloudType = Object.keys(config.cloudPatterns)[index];
}
});
return {
links: [...new Set(links)],
cloudType,
};
}
async searchAll(keyword: string) {
const allResults = [];
for (let i = 0; i < config.rss.channels.length; i++) {
const channel = config.rss.channels[i];
try {
const rssUrl = `${config.rss.baseUrl}/${
channel.id
}${keyword ? `/searchQuery=${encodeURIComponent(keyword)}` : ""}`;
const results = await this.searchInRSSFeed(rssUrl);
if (results.items.length > 0) {
const channelResults = results.items
.filter((item: RSSItem) => item.cloudLinks && item.cloudLinks.length > 0)
.map((item: RSSItem) => ({
...item,
channel: channel.name + "(" + channel.id + ")",
}));
allResults.push(...channelResults);
}
} catch (error) {
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
}
}
return {
data: allResults,
message: "搜索成功",
};
}
async searchInRSSFeed(rssUrl: string) {
try {
const response = await this.axiosInstance.get(rssUrl);
const feed = await this.parser.parseString(response.data);
return {
items: feed.items.map((item: RSSItem) => {
const linkInfo = this.extractCloudLinks(item.content || item.description || "");
return {
title: item.title || "",
link: item.link || "",
pubDate: item.pubDate || "",
image: item.image || "",
cloudLinks: linkInfo.links,
cloudType: linkInfo.cloudType,
};
}),
};
} catch (error) {
Logger.error(`RSS源解析错误: ${rssUrl}`, error);
return {
items: [],
};
}
}
}

View File

@@ -27,7 +27,7 @@ export class Searcher {
this.initializeAxiosInstance(); this.initializeAxiosInstance();
} }
private async initializeAxiosInstance(isUpdate = false) { private async initializeAxiosInstance(isUpdate = false): Promise<void> {
let settings = null; let settings = null;
if (isUpdate) { if (isUpdate) {
settings = await GlobalSetting.findOne(); settings = await GlobalSetting.findOne();
@@ -78,7 +78,6 @@ export class Searcher {
async searchAll(keyword: string, channelId?: string, messageId?: string) { async searchAll(keyword: string, channelId?: string, messageId?: string) {
const allResults = []; const allResults = [];
const totalChannels = config.rss.channels.length;
const channelList = channelId const channelList = channelId
? config.rss.channels.filter((channel) => channel.id === channelId) ? config.rss.channels.filter((channel) => channel.id === channelId)
@@ -90,7 +89,7 @@ export class Searcher {
const messageIdparams = messageId ? `before=${messageId}` : ""; const messageIdparams = messageId ? `before=${messageId}` : "";
const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`; const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`;
console.log(`Searching in channel ${channel.name} with URL: ${url}`); console.log(`Searching in channel ${channel.name} with URL: ${url}`);
const results = await this.searchInWeb(url, channel.id); const results = await this.searchInWeb(url);
console.log(`Found ${results.items.length} items in channel ${channel.name}`); console.log(`Found ${results.items.length} items in channel ${channel.name}`);
if (results.items.length > 0) { if (results.items.length > 0) {
const channelResults = results.items const channelResults = results.items
@@ -120,7 +119,7 @@ export class Searcher {
}; };
} }
async searchInWeb(url: string, channelId: string) { async searchInWeb(url: string) {
try { try {
if (!this.axiosInstance) { if (!this.axiosInstance) {
throw new Error("Axios instance is not initialized"); throw new Error("Axios instance is not initialized");

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Request } from "express"; import { Request } from "express";
declare module "express" { declare module "express" {

View File

@@ -1,7 +1,5 @@
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios"; import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
import tunnel from "tunnel"; import tunnel from "tunnel";
import { config } from "../config";
import GlobalSetting from "../models/GlobalSetting";
interface ProxyConfig { interface ProxyConfig {
host: string; host: string;

View File

@@ -1,8 +1,15 @@
import { Response, NextFunction } from "express"; import { Response, NextFunction } from "express";
import { Logger } from "../utils/logger"; import { Logger } from "../utils/logger";
interface CustomError {
name?: string;
message: string;
success?: boolean;
}
export default function handleError( export default function handleError(
res: Response, res: Response,
error: any, error: CustomError | unknown,
message: string, message: string,
next: NextFunction next: NextFunction
) { ) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

BIN
docs/images/wechat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View File

@@ -8,13 +8,12 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AsideMenu: typeof import('./src/components/AsideMenu.vue')['default'] AsideMenu: typeof import('./src/components/AsideMenu.vue')['default']
copy: typeof import('./src/components/Home/FolderSelect copy.vue')['default']
DoubanMovie: typeof import('./src/components/Home/DoubanMovie.vue')['default'] DoubanMovie: typeof import('./src/components/Home/DoubanMovie.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside'] ElAside: typeof import('element-plus/es')['ElAside']
ElBacktop: typeof import('element-plus/es')['ElBacktop'] ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"]
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -30,8 +29,6 @@ declare module 'vue' {
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
@@ -43,7 +40,7 @@ declare module 'vue' {
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']
FolderSelect: typeof import('./src/components/Home/FolderSelect.vue')['default'] FolderSelect: typeof import('./src/components/Home/FolderSelect.vue')['default']
ResourceCard: typeof import('./src/components/Home/ResourceCard.vue')['default'] ResourceCard: typeof import('./src/components/Home/ResourceCard.vue')['default']
ResourceList: typeof import('./src/components/Home/ResourceList.vue')['default'] ResourceSelect: typeof import('./src/components/Home/ResourceSelect.vue')['default']
ResourceTable: typeof import('./src/components/Home/ResourceTable.vue')['default'] ResourceTable: typeof import('./src/components/Home/ResourceTable.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

View File

@@ -5,32 +5,32 @@
</template> </template>
<style> <style>
#app { #app {
height: 100vh; height: 100vh;
} }
:root { :root {
--theme-color: #3e3e3e; --theme-color: #3e3e3e;
--theme-theme: #133ab3; --theme-theme: #133ab3;
--theme-background: #fafafa; --theme-background: #fafafa;
--theme-other_background: #ffffff; --theme-other_background: #ffffff;
} }
html, html,
body { body {
margin: 0; margin: 0;
font-size: 15px; font-size: 15px;
font-family: font-family:
v-sans, v-sans,
system-ui, system-ui,
-apple-system, -apple-system,
BlinkMacSystemFont, BlinkMacSystemFont,
Segoe UI, Segoe UI,
sans-serif, sans-serif,
"Apple Color Emoji", "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Emoji",
Segoe UI Symbol; Segoe UI Symbol;
line-height: 1.6; line-height: 1.6;
color: var(--theme-color); color: var(--theme-color);
background-color: var(--theme-background); background-color: var(--theme-background);
word-wrap: break-word; word-wrap: break-word;
} }
</style> </style>

View File

@@ -12,15 +12,15 @@
@close="handleClose" @close="handleClose"
> >
<template v-for="menu in menuList"> <template v-for="menu in menuList">
<el-sub-menu :index="menu.index" v-if="menu.children"> <el-sub-menu v-if="menu.children" :key="menu.index" :index="menu.index">
<template #title> <template #title>
<el-icon><component :is="menu.icon" /></el-icon> <el-icon><component :is="menu.icon" /></el-icon>
<span>{{ menu.title }}</span> <span>{{ menu.title }}</span>
</template> </template>
<el-menu-item <el-menu-item
v-for="child in menu.children" v-for="child in menu.children"
:index="child.index"
:key="child.index" :key="child.index"
:index="child.index"
@click="handleMenuClick(child)" @click="handleMenuClick(child)"
> >
{{ child.title }} {{ child.title }}
@@ -28,9 +28,10 @@
</el-sub-menu> </el-sub-menu>
<el-menu-item <el-menu-item
v-else v-else
:key="menu.router"
:index="menu.index" :index="menu.index"
@click="handleMenuClick(menu)"
:disabled="menu.disabled" :disabled="menu.disabled"
@click="handleMenuClick(menu)"
> >
<el-icon><component :is="menu.icon" /></el-icon> <el-icon><component :is="menu.icon" /></el-icon>
<span>{{ menu.title }}</span> <span>{{ menu.title }}</span>
@@ -41,115 +42,108 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Search, Film, Setting } from "@element-plus/icons-vue"; import { Search, Film, Setting } from "@element-plus/icons-vue";
import logo from "@/assets/images/logo.png"; import logo from "@/assets/images/logo.png";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { computed } from "vue"; import { computed } from "vue";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
interface MenuItem { interface MenuItem {
index: string; index: string;
title: string; title: string;
icon?: any; icon?: typeof Search | typeof Film | typeof Setting;
router?: string; router?: string;
children?: MenuItem[]; children?: MenuItem[];
disabled?: boolean; disabled?: boolean;
}
const menuList: MenuItem[] = [
{
index: "2",
title: "资源搜索",
icon: Search,
router: "/",
},
{
index: "1",
title: "豆瓣榜单",
icon: Film,
children: [
{
index: "1-1",
title: "热门电影",
router: "/douban?type=movie",
},
{
index: "1-2",
title: "热门电视剧",
router: "/douban?type=tv",
},
{
index: "1-3",
title: "最新电影",
router: "/douban?type=movie&tag=最新",
},
{
index: "1-4",
title: "热门综艺",
router: "/douban?type=tv&tag=综艺",
},
],
},
{
index: "3",
title: "设置",
icon: Setting,
router: "/setting",
disabled: false,
},
];
const currentMenu = computed(() => {
return menuList
.reduce((pre: MenuItem[], cur: MenuItem) => {
if (!cur.children) {
pre.push(cur);
} else {
pre.push(...cur.children);
}
return pre;
}, [])
.find((x) => x.router === decodeURIComponent(route.fullPath));
});
const currentMenuOpen = computed(() => {
if (currentMenu.value && currentMenu.value.index.length > 1) {
return [currentMenu.value.index.split("-")[0]];
} else {
return [];
} }
});
const menuList: MenuItem[] = [ const handleOpen = (_key: string, _keyPath: string[]): void => {};
{ const handleClose = (_key: string, _keyPath: string[]): void => {};
index: "2", const handleMenuClick = (menu: MenuItem): void => {
title: "资源搜索", if (menu.router) {
icon: Search, router.push(menu.router);
router: "/", }
}, };
{
index: "1",
title: "豆瓣榜单",
icon: Film,
children: [
{
index: "1-1",
title: "热门电影",
router: "/douban?type=movie",
},
{
index: "1-2",
title: "热门电视剧",
router: "/douban?type=tv",
},
{
index: "1-3",
title: "最新电影",
router: "/douban?type=movie&tag=最新",
},
{
index: "1-4",
title: "热门综艺",
router: "/douban?type=tv&tag=综艺",
},
],
},
{
index: "3",
title: "设置",
icon: Setting,
router: "/setting",
disabled: false,
},
];
const currentMenu = computed(() => {
console.log("route", decodeURIComponent(route.fullPath));
return menuList
.reduce((pre: MenuItem[], cur: MenuItem) => {
if (!cur.children) {
pre.push(cur);
} else {
pre.push(...cur.children);
}
return pre;
}, [])
.find((x) => x.router === decodeURIComponent(route.fullPath));
});
const currentMenuOpen = computed(() => {
if (currentMenu.value && currentMenu.value.index.length > 1) {
console.log([currentMenu.value.index.split("-")[0]]);
return [currentMenu.value.index.split("-")[0]];
} else {
return [];
}
});
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath);
};
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath);
};
const handleMenuClick = (menu: any) => {
console.log(menu);
if (menu.router) {
router.push(menu.router);
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.el-menu-vertical { .el-menu-vertical {
width: 100%; width: 100%;
height: 100vh; height: 100vh;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
.logo-img {
width: 30px;
height: 30px;
margin-right: 15px;
} }
.logo { .logo-text {
display: flex; font-size: 20px;
align-items: center;
justify-content: center;
padding: 10px 0;
.logo-img {
width: 30px;
height: 30px;
margin-right: 15px;
}
.logo-text {
font-size: 20px;
}
} }
}
</style> </style>

View File

@@ -11,8 +11,8 @@
node-key="cid" node-key="cid"
:load="loadNode" :load="loadNode"
lazy lazy
@node-click="handleNodeClick"
highlight-current highlight-current
@node-click="handleNodeClick"
> >
<template #default="{ node }"> <template #default="{ node }">
<span class="folder-node"> <span class="folder-node">
@@ -25,107 +25,114 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps } from "vue"; import { ref, defineProps } from "vue";
import { cloud115Api } from "@/api/cloud115"; import { cloud115Api } from "@/api/cloud115";
import { quarkApi } from "@/api/quark"; import { quarkApi } from "@/api/quark";
import type { TreeInstance } from "element-plus"; import type { TreeInstance } from "element-plus";
import type { Folder } from "@/types"; import type { Folder } from "@/types";
import { type RequestResult } from "@/types/response"; import { type RequestResult } from "@/types/response";
import { ElMessage } from "element-plus"; import { useResourceStore } from "@/stores/resource";
const props = defineProps({ const resourceStore = useResourceStore();
cloudType: { import { ElMessage } from "element-plus";
type: String,
required: true,
},
});
const treeRef = ref<TreeInstance>(); const props = defineProps({
const folders = ref<Folder[]>([]); cloudType: {
const selectedFolder = ref<Folder | null>(null); type: String,
const emit = defineEmits<{ required: true,
(e: "select", folderId: string): void; },
(e: "close"): void; });
}>();
const defaultProps = { const treeRef = ref<TreeInstance>();
label: "name", const folders = ref<Folder[]>([]);
children: "children", const selectedFolder = ref<Folder | null>(null);
isLeaf: "leaf", const emit = defineEmits<{
}; (e: "select", folderId: string): void;
(e: "close"): void;
}>();
const cloudTypeApiMap = { const defaultProps = {
pan115: cloud115Api, label: "name",
quark: quarkApi, children: "children",
}; isLeaf: "leaf",
};
const loadNode = async (node: any, resolve: (list: Folder[]) => void) => { const cloudTypeApiMap = {
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap]; pan115: cloud115Api,
try { quark: quarkApi,
let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" }; };
if (node.level === 0) {
if (api.getFolderList) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
// 使用类型保护检查方法是否存在 const loadNode = async (node: any, resolve: (list: Folder[]) => void) => {
res = await api.getFolderList(); const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
} try {
} else { let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" };
if (api.getFolderList) { resourceStore.setLoadTree(true);
// 使用类型保护检查方法是否存在 if (node.level === 0) {
res = await api.getFolderList(node.data.cid); if (api.getFolderList) {
} // 使用类型保护检查方法是否存在
res = await api.getFolderList();
} }
if (res?.code === 0) { } else {
resolve(res.data.length ? res.data : []); if (api.getFolderList) {
} else { // 使用类型保护检查方法是否存在
throw new Error(res.message); res = await api.getFolderList(node.data.cid);
} }
} catch (error) {
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");
// 关闭模态框
emit("close");
resolve([]);
} }
}; if (res?.code === 0) {
resolve(res.data.length ? res.data : []);
} else {
throw new Error(res.message);
}
resourceStore.setLoadTree(false);
} catch (error) {
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");
// 关闭模态框
emit("close");
resourceStore.setLoadTree(false);
resolve([]);
}
};
const handleNodeClick = (data: Folder) => { const handleNodeClick = (data: Folder) => {
selectedFolder.value = { selectedFolder.value = {
...data, ...data,
path: data.path ? [...data.path, data] : [data], path: data.path ? [...data.path, data] : [data],
};
emit("select", data.cid);
}; };
emit("select", data.cid);
};
</script> </script>
<style scoped> <style scoped>
.folder-select { .folder-select {
min-height: 300px; min-height: 300px;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
} }
.folder-node { .folder-node {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.folder-path { .folder-path {
color: #999; color: #999;
font-size: 12px; font-size: 12px;
margin-left: 8px; margin-left: 8px;
} }
:deep(.el-tree-node__content) { :deep(.el-tree-node__content) {
height: 32px; height: 32px;
} }
.folder-select-header { .folder-select-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
margin-bottom: 10px; margin-bottom: 10px;
font-size: 14px; font-size: 14px;
padding: 5px 10px; padding: 5px 10px;
border: 1px solid #e5e6e8; border: 1px solid #e5e6e8;
border-radius: 8px; border-radius: 8px;
} }
</style> </style>

View File

@@ -10,7 +10,7 @@
><ArrowDown ><ArrowDown
/></el-icon> /></el-icon>
</div> </div>
<div class="card-item-list" v-show="group.displayList"> <div v-show="group.displayList" class="card-item-list">
<div v-for="resource in group.list" :key="resource.messageId" class="card-item-content"> <div v-for="resource in group.list" :key="resource.messageId" class="card-item-content">
<el-card class="card-item"> <el-card class="card-item">
<el-image <el-image
@@ -28,12 +28,12 @@
><div class="item-name">{{ resource.title }}</div></el-link ><div class="item-name">{{ resource.title }}</div></el-link
> >
<div class="item-description" v-html="resource.content"></div> <div class="item-description" v-html="resource.content"></div>
<div class="tags-list" v-if="resource.tags && resource.tags.length"> <div v-if="resource.tags && resource.tags.length" class="tags-list">
<span>标签</span> <span>标签</span>
<el-tag <el-tag
v-for="item in resource.tags" v-for="item in resource.tags"
class="resource_tag"
:key="item" :key="item"
class="resource_tag"
@click="searchMovieforTag(item)" @click="searchMovieforTag(item)"
> >
{{ item }} {{ item }}
@@ -54,7 +54,7 @@
</el-card> </el-card>
</div> </div>
</div> </div>
<div class="load-more" v-show="group.displayList"> <div v-show="group.displayList" class="load-more">
<el-button @click="handleLoadMore(group.id)"> 加载更多 </el-button> <el-button @click="handleLoadMore(group.id)"> 加载更多 </el-button>
</div> </div>
</div> </div>
@@ -62,128 +62,128 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
import { computed } from "vue"; import { computed } from "vue";
import type { ResourceItem, TagColor } from "@/types"; import type { ResourceItem, TagColor } from "@/types";
const store = useResourceStore(); const store = useResourceStore();
const location = computed(() => window.location); const location = computed(() => window.location);
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]); const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
const handleSave = (resource: ResourceItem) => { const handleSave = (resource: ResourceItem) => {
emit("save", resource); emit("save", resource);
}; };
const searchMovieforTag = (tag: string) => { const searchMovieforTag = (tag: string) => {
emit("searchMovieforTag", tag); emit("searchMovieforTag", tag);
}; };
const handleLoadMore = (channelId: string) => { const handleLoadMore = (channelId: string) => {
emit("loadMore", channelId); emit("loadMore", channelId);
}; };
</script> </script>
<style scoped> <style scoped>
.resource-list { .resource-list {
margin-bottom: 30px; margin-bottom: 30px;
padding: 20px; padding: 20px;
border-radius: 15px; border-radius: 15px;
background-color: var(--theme-other_background); background-color: var(--theme-other_background);
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
}
} }
.card-item-list { }
display: grid; .card-item-list {
grid-template-columns: repeat(auto-fill, 220px); display: grid;
grid-row-gap: 30px; grid-template-columns: repeat(auto-fill, 220px);
justify-content: space-between; grid-row-gap: 30px;
margin-top: 20px; justify-content: space-between;
/* grid-column-gap: auto-fill; */ margin-top: 20px;
/* flex-wrap: wrap; */ /* grid-column-gap: auto-fill; */
/* flex-wrap: wrap; */
}
.card-item-content {
/* height: 520px; */
}
.channel-logo {
height: 40px;
width: 40px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-right: 10px;
}
.load-more {
margin-top: 40px;
width: 100%;
text-align: center;
}
.card-item {
max-width: 480px;
height: 100%;
border-radius: 20px;
}
.card-item-image {
border-radius: 20px;
width: 100%;
height: 220px;
}
.item-name,
.item-description {
max-width: 100%;
margin: 15px 0;
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
white-space: all;
}
.item-description {
-webkit-line-clamp: 4;
margin-top: 0;
height: 100px;
}
.item-name {
height: 58px;
font-size: 18px;
}
.tags-list {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
height: 58px;
overflow: hidden;
}
.resource_tag {
cursor: pointer;
margin-right: 10px;
margin-bottom: 5px;
}
.group-header {
height: 50px;
line-height: 50px;
text-align: left;
padding: 0 15px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: space-between;
/* text-align: center; */
.el-link {
font-size: 22px;
} }
.card-item-content { .header-icon {
/* height: 520px; */
}
.channel-logo {
height: 40px;
width: 40px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-right: 10px;
}
.load-more {
margin-top: 40px;
width: 100%;
text-align: center;
}
.card-item {
max-width: 480px;
height: 100%;
border-radius: 20px;
}
.card-item-image {
border-radius: 20px;
width: 100%;
height: 220px;
}
.item-name,
.item-description {
max-width: 100%;
margin: 15px 0;
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
white-space: all;
}
.item-description {
-webkit-line-clamp: 4;
margin-top: 0;
height: 100px;
}
.item-name {
height: 58px;
font-size: 18px;
}
.tags-list {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
height: 58px;
overflow: hidden;
}
.resource_tag {
cursor: pointer; cursor: pointer;
margin-right: 10px; width: 50px;
margin-bottom: 5px;
}
.group-header {
height: 50px; height: 50px;
line-height: 50px;
text-align: left;
padding: 0 15px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: space-between;
/* text-align: center; */
.el-link {
font-size: 22px;
}
.header-icon {
cursor: pointer;
width: 50px;
height: 50px;
}
}
.item-footer {
display: flex;
align-items: center;
justify-content: space-between;
} }
}
.item-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
</style> </style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="folder-select">
<el-tree
ref="treeRef"
:data="resourceStore.shareInfo.list"
:props="defaultProps"
:default-checked-keys="resourceStore.shareInfo.list?.map((x) => x.fileId) || []"
node-key="fileId"
show-checkbox
highlight-current
@check-change="handleCheckChange"
>
<template #default="{ node }">
<span class="folder-node">
<el-icon><Folder /></el-icon>
{{ node.data.fileName }}
<span v-if="node.data.fileSize" style="font-weight: bold"
>({{ formattedFileSize(node.data.fileSize) }})</span
>
</span>
</template>
</el-tree>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useResourceStore } from "@/stores/resource";
import { formattedFileSize } from "@/utils/index";
import type { ShareInfo } from "@/types";
const resourceStore = useResourceStore();
const selectedResource = ref<ShareInfo[]>([]);
const defaultProps = {
isLeaf: "leaf",
};
const handleCheckChange = (data: ShareInfo) => {
selectedResource.value = [...resourceStore.resourceSelect, ...selectedResource.value];
if (selectedResource.value.findIndex((x) => x.fileId === data.fileId) === -1) {
selectedResource.value.push(data);
} else {
selectedResource.value = selectedResource.value.filter((x) => x.fileId !== data.fileId);
}
resourceStore.setSelectedResource(selectedResource.value);
};
</script>
<style scoped>
.folder-select {
min-height: 300px;
max-height: 500px;
overflow-y: auto;
}
.folder-node {
display: flex;
align-items: center;
gap: 8px;
}
.folder-path {
color: #999;
font-size: 12px;
margin-left: 8px;
}
:deep(.el-tree-node__content) {
height: 32px;
}
.folder-select-header {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 10px;
font-size: 14px;
padding: 5px 10px;
border: 1px solid #e5e6e8;
border-radius: 8px;
}
</style>

View File

@@ -13,8 +13,8 @@
<el-table-column label="图片" width="180"> <el-table-column label="图片" width="180">
<template #default="{ row }"> <template #default="{ row }">
<el-image <el-image
class="table-item-image"
v-if="row.image" v-if="row.image"
class="table-item-image"
:src="`/tele-images/?url=${encodeURIComponent(row.image as string)}`" :src="`/tele-images/?url=${encodeURIComponent(row.image as string)}`"
hide-on-click-modal hide-on-click-modal
:preview-src-list="[ :preview-src-list="[
@@ -42,17 +42,17 @@
</el-table-column> </el-table-column>
<el-table-column prop="title" label="描述"> <el-table-column prop="title" label="描述">
<template #default="{ row }"> <template #default="{ row }">
<div v-html="row.content" class="item-description"></div> <div class="item-description" v-html="row.content"></div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="tags" label="标签"> <el-table-column prop="tags" label="标签">
<template #default="{ row }"> <template #default="{ row }">
<div class="tags-list" v-if="row.tags.length > 0"> <div v-if="row.tags.length > 0" class="tags-list">
<span>标签</span> <span>标签</span>
<el-tag <el-tag
v-for="item in row.tags" v-for="item in row.tags"
class="resource_tag"
:key="item" :key="item"
class="resource_tag"
@click="searchMovieforTag(item)" @click="searchMovieforTag(item)"
> >
{{ item }} {{ item }}
@@ -102,96 +102,97 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
import type { Resource, TagColor } from "@/types"; import type { Resource, TagColor } from "@/types";
import { computed } from "vue"; import { computed } from "vue";
const store = useResourceStore(); const store = useResourceStore();
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]); const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
const location = computed(() => window.location); const location = computed(() => window.location);
const handleSave = (resource: Resource) => { const handleSave = (resource: Resource) => {
emit("save", resource); emit("save", resource);
}; };
// 添加加载更多处理函数 // 添加加载更多处理函数
const handleLoadMore = (channelId: string) => { const handleLoadMore = (channelId: string) => {
emit("loadMore", channelId); emit("loadMore", channelId);
}; };
const searchMovieforTag = (tag: string) => { const searchMovieforTag = (tag: string) => {
emit("searchMovieforTag", tag); emit("searchMovieforTag", tag);
}; };
</script> </script>
<style scoped> <style scoped>
.resource-list-table { .resource-list-table {
border-radius: 15px; border-radius: 15px;
} }
.group-header { .group-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.channel-logo { .channel-logo {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: 10px; margin-right: 10px;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
} }
.table-item-image { .table-item-image {
border-radius: 20px; border-radius: 20px;
width: 100%; width: 100%;
height: 220px; height: 220px;
} }
.item-count { .item-count {
color: #909399; color: #909399;
font-size: 0.9em; font-size: 0.9em;
} }
.tags-list { .tags-list {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
} }
.resource_tag { .resource_tag {
cursor: pointer; cursor: pointer;
margin-right: 10px; margin-right: 10px;
margin-bottom: 5px; margin-bottom: 5px;
} }
.item-description { .item-description {
max-width: 100%; max-width: 100%;
margin: 15px 0; margin: 15px 0;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 4; line-clamp: 4;
overflow: hidden; -webkit-line-clamp: 4;
white-space: all; overflow: hidden;
} white-space: all;
}
:deep(.el-table__expand-column) { :deep(.el-table__expand-column) {
.cell { .cell {
padding: 0 !important; padding: 0 !important;
}
} }
}
:deep(.el-table__expanded-cell) { :deep(.el-table__expanded-cell) {
padding: 20px !important; padding: 20px !important;
} }
:deep(.el-table__expand-icon) { :deep(.el-table__expand-icon) {
height: 23px; height: 23px;
line-height: 23px; line-height: 23px;
} }
.load-more { .load-more {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 16px 0; padding: 16px 0;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
declare module "*.vue" { declare module "*.vue" {
import type { DefineComponent } from "vue"; import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>; const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
export default component; export default component;
} }

View File

@@ -2,15 +2,15 @@ import { createApp } from "vue";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import ElementPlus from "element-plus"; import ElementPlus from "element-plus";
import "element-plus/dist/index.css"; import "element-plus/dist/index.css";
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router/index"; import router from "./router/index";
const app = createApp(App); const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) app.component(key, component);
} }
app.use(createPinia()); app.use(createPinia());
app.use(router); app.use(router);

View File

@@ -7,6 +7,7 @@ import type {
ShareInfoResponse, ShareInfoResponse,
Save115FileParams, Save115FileParams,
SaveQuarkFileParams, SaveQuarkFileParams,
ShareInfo,
ResourceItem, ResourceItem,
} from "@/types"; } from "@/types";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
@@ -23,20 +24,26 @@ const lastResource = (
) as StorageListObject; ) as StorageListObject;
// 定义云盘驱动配置类型 // 定义云盘驱动配置类型
interface CloudDriveConfig { interface CloudDriveConfig<
T extends Record<string, string>,
P extends Save115FileParams | SaveQuarkFileParams,
> {
name: string; name: string;
type: string; type: string;
regex: RegExp; regex: RegExp;
api: { api: {
getShareInfo: (parsedCode: any) => Promise<ShareInfoResponse>; getShareInfo: (parsedCode: T) => Promise<ShareInfoResponse>;
saveFile: (params: Record<string, any>) => Promise<any>; saveFile: (params: P) => Promise<{ code: number; message?: string }>;
}; };
parseShareCode: (match: RegExpMatchArray) => Record<string, string>; parseShareCode: (match: RegExpMatchArray) => T;
getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => Record<string, any>; getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => P;
} }
// 云盘类型配置 // 云盘类型配置
export const CLOUD_DRIVES: CloudDriveConfig[] = [ export const CLOUD_DRIVES: [
CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams>,
CloudDriveConfig<{ pwdId: string }, SaveQuarkFileParams>,
] = [
{ {
name: "115网盘", name: "115网盘",
type: "pan115", type: "pan115",
@@ -44,15 +51,17 @@ export const CLOUD_DRIVES: CloudDriveConfig[] = [
api: { api: {
getShareInfo: (parsedCode: { shareCode: string; receiveCode: string }) => getShareInfo: (parsedCode: { shareCode: string; receiveCode: string }) =>
cloud115Api.getShareInfo(parsedCode.shareCode, parsedCode.receiveCode), cloud115Api.getShareInfo(parsedCode.shareCode, parsedCode.receiveCode),
saveFile: async (params) => await cloud115Api.saveFile(params as Save115FileParams), saveFile: async (params: Save115FileParams) => {
return await cloud115Api.saveFile(params as Save115FileParams);
},
}, },
parseShareCode: (match) => ({ parseShareCode: (match: RegExpMatchArray) => ({
shareCode: match[1], shareCode: match[1],
receiveCode: match[2] || "", receiveCode: match[2] || "",
}), }),
getSaveParams: (shareInfo, folderId) => ({ getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => ({
shareCode: shareInfo.shareCode, shareCode: shareInfo.shareCode || "",
receiveCode: shareInfo.receiveCode, receiveCode: shareInfo.receiveCode || "",
fileId: shareInfo.list[0].fileId, fileId: shareInfo.list[0].fileId,
folderId, folderId,
}), }),
@@ -63,12 +72,16 @@ export const CLOUD_DRIVES: CloudDriveConfig[] = [
regex: /pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/, regex: /pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/,
api: { api: {
getShareInfo: (parsedCode: { pwdId: string }) => quarkApi.getShareInfo(parsedCode.pwdId), getShareInfo: (parsedCode: { pwdId: string }) => quarkApi.getShareInfo(parsedCode.pwdId),
saveFile: async (params) => await quarkApi.saveFile(params as SaveQuarkFileParams), saveFile: async (params: SaveQuarkFileParams) => {
return await quarkApi.saveFile(params as SaveQuarkFileParams);
},
}, },
parseShareCode: (match) => ({ pwdId: match[1] }), parseShareCode: (match: RegExpMatchArray) => ({ pwdId: match[1] }),
getSaveParams: (shareInfo, folderId) => ({ getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => ({
fid_list: shareInfo.list.map((item) => item.fileId || ""), fid_list: shareInfo.list.map((item: { fileId?: string }) => item.fileId || ""),
fid_token_list: shareInfo.list.map((item) => item.fileIdToken || ""), fid_token_list: shareInfo.list.map(
(item: { fileIdToken?: string }) => item.fileIdToken || ""
),
to_pdir_fid: folderId, to_pdir_fid: folderId,
pwd_id: shareInfo.pwdId || "", pwd_id: shareInfo.pwdId || "",
stoken: shareInfo.stoken || "", stoken: shareInfo.stoken || "",
@@ -89,13 +102,18 @@ export const useResourceStore = defineStore("resource", {
}, },
resources: lastResource.list, resources: lastResource.list,
lastUpdateTime: lastResource.lastUpdateTime || "", lastUpdateTime: lastResource.lastUpdateTime || "",
selectedResources: [] as Resource[], shareInfo: {} as ShareInfoResponse,
resourceSelect: [] as ShareInfo[],
loading: false, loading: false,
lastKeyWord: "", lastKeyWord: "",
backupPlan: false, backupPlan: false,
loadTree: false,
}), }),
actions: { actions: {
setLoadTree(loadTree: boolean) {
this.loadTree = loadTree;
},
// 搜索资源 // 搜索资源
async searchResources(keyword?: string, isLoadMore = false, channelId?: string): Promise<void> { async searchResources(keyword?: string, isLoadMore = false, channelId?: string): Promise<void> {
this.loading = true; this.loading = true;
@@ -141,6 +159,11 @@ export const useResourceStore = defineStore("resource", {
} }
}, },
// 设置选择资源
async setSelectedResource(resourceSelect: ShareInfo[]) {
this.resourceSelect = resourceSelect;
},
// 转存资源 // 转存资源
async saveResource(resource: ResourceItem, folderId: string): Promise<void> { async saveResource(resource: ResourceItem, folderId: string): Promise<void> {
const savePromises: Promise<void>[] = []; const savePromises: Promise<void>[] = [];
@@ -156,7 +179,9 @@ export const useResourceStore = defineStore("resource", {
async saveResourceToDrive( async saveResourceToDrive(
resource: ResourceItem, resource: ResourceItem,
folderId: string, folderId: string,
drive: CloudDriveConfig drive:
| CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams>
| CloudDriveConfig<{ pwdId: string }, SaveQuarkFileParams>
): Promise<void> { ): Promise<void> {
const link = resource.cloudLinks.find((link) => drive.regex.test(link)); const link = resource.cloudLinks.find((link) => drive.regex.test(link));
if (!link) return; if (!link) return;
@@ -164,29 +189,29 @@ export const useResourceStore = defineStore("resource", {
const match = link.match(drive.regex); const match = link.match(drive.regex);
if (!match) throw new Error("链接解析失败"); if (!match) throw new Error("链接解析失败");
const parsedCode = drive.parseShareCode(match); const shareInfo = {
...this.shareInfo,
list: this.resourceSelect,
};
let shareInfo = await drive.api.getShareInfo(parsedCode); if (this.is115Drive(drive)) {
if (shareInfo) { const params = drive.getSaveParams(shareInfo, folderId);
if (Array.isArray(shareInfo)) { const result = await drive.api.saveFile(params);
shareInfo = {
list: shareInfo, if (result.code === 0) {
...parsedCode, ElMessage.success(`${drive.name} 转存成功`);
};
} else { } else {
shareInfo = { ElMessage.error(result.message);
...shareInfo,
...parsedCode,
};
} }
}
const params = drive.getSaveParams(shareInfo, folderId);
const result = await drive.api.saveFile(params);
if (result.code === 0) {
ElMessage.success(`${drive.name} 转存成功`);
} else { } else {
ElMessage.error(result.message); const params = drive.getSaveParams(shareInfo, folderId);
const result = await drive.api.saveFile(params);
if (result.code === 0) {
ElMessage.success(`${drive.name} 转存成功`);
} else {
ElMessage.error(result.message);
}
} }
}, },
@@ -202,7 +227,12 @@ export const useResourceStore = defineStore("resource", {
if (!match) throw new Error("链接解析失败"); if (!match) throw new Error("链接解析失败");
const parsedCode = matchedDrive.parseShareCode(match); const parsedCode = matchedDrive.parseShareCode(match);
let shareInfo = await matchedDrive.api.getShareInfo(parsedCode); let shareInfo = this.is115Drive(matchedDrive)
? await matchedDrive.api.getShareInfo(
parsedCode as { shareCode: string; receiveCode: string }
)
: await matchedDrive.api.getShareInfo(parsedCode as { pwdId: string });
if (Array.isArray(shareInfo)) { if (Array.isArray(shareInfo)) {
shareInfo = { shareInfo = {
list: shareInfo, list: shareInfo,
@@ -241,10 +271,65 @@ export const useResourceStore = defineStore("resource", {
} }
}, },
// 获取资源列表并选择
async getResourceListAndSelect(resource: ResourceItem): Promise<boolean> {
const { cloudType } = resource;
const drive = CLOUD_DRIVES.find((x) => x.type === cloudType);
if (!drive) {
return false;
}
const link = resource.cloudLinks.find((link) => drive.regex.test(link));
if (!link) return false;
const match = link.match(drive.regex);
if (!match) throw new Error("链接解析失败");
const parsedCode = drive.parseShareCode(match);
let shareInfo = {} as ShareInfoResponse;
this.setLoadTree(true);
if (this.is115Drive(drive)) {
shareInfo = await drive.api.getShareInfo(
parsedCode as { shareCode: string; receiveCode: string }
);
} else {
shareInfo = this.is115Drive(drive)
? await drive.api.getShareInfo(parsedCode as { shareCode: string; receiveCode: string })
: await drive.api.getShareInfo(parsedCode as { pwdId: string });
}
this.setLoadTree(false);
if (shareInfo) {
if (Array.isArray(shareInfo)) {
shareInfo = {
list: shareInfo,
...parsedCode,
};
} else {
shareInfo = {
...shareInfo,
...parsedCode,
};
}
this.shareInfo = shareInfo;
this.setSelectedResource(this.shareInfo.list);
return true;
} else {
ElMessage.error("获取资源信息失败,请先检查cookie!");
return false;
}
},
// 统一错误处理 // 统一错误处理
handleError(message: string, error: unknown): void { handleError(message: string, error: unknown): void {
console.error(message, error); console.error(message, error);
ElMessage.error(error instanceof Error ? error.message : message); ElMessage.error(error instanceof Error ? error.message : message);
}, },
is115Drive(
drive:
| CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams>
| CloudDriveConfig<{ pwdId: string }, SaveQuarkFileParams>
): drive is CloudDriveConfig<{ shareCode: string; receiveCode: string }, Save115FileParams> {
return drive.type === "pan115";
},
}, },
}); });

View File

@@ -1,11 +1,11 @@
:root { :root {
--theme-color: #3e3e3e; --theme-color: #3e3e3e;
--theme-theme: #133ab3; --theme-theme: #133ab3;
--theme-background: #fafafa; --theme-background: #fafafa;
--theme-other_background: #ffffff; --theme-other_background: #ffffff;
} }
input { input {
border: none; border: none;
outline: none; outline: none;
} }

View File

@@ -26,7 +26,7 @@ export interface Resource {
export interface ShareInfo { export interface ShareInfo {
fileId: string; fileId: string;
fileName: string; fileName: string;
fileSize: number; fileSize?: number;
fileIdToken?: string; fileIdToken?: string;
} }
@@ -36,6 +36,7 @@ export interface ShareInfoResponse {
stoken?: string; stoken?: string;
shareCode?: string; shareCode?: string;
receiveCode?: string; receiveCode?: string;
fileSize?: number;
} }
export interface Folder { export interface Folder {
@@ -51,7 +52,7 @@ export interface SaveFileParams {
folderId: string; folderId: string;
} }
export interface ApiResponse<T = any> { export interface ApiResponse<T = unknown> {
success: boolean; success: boolean;
data?: T; data?: T;
error?: string; error?: string;

View File

@@ -0,0 +1,9 @@
export const formattedFileSize = (size: number): string => {
if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(2)}KB`;
}
if (size < 1024 * 1024 * 1024) {
return `${(size / 1024 / 1024).toFixed(2)}MB`;
}
return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`;
};

View File

@@ -1,10 +1,10 @@
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { RequestResult } from "../types/response"; import { RequestResult } from "../types/response";
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL as string, baseURL: import.meta.env.VITE_API_BASE_URL as string,
timeout: 9000, timeout: 16000,
withCredentials: true, withCredentials: true,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -49,13 +49,17 @@ axiosInstance.interceptors.response.use(
); );
const request = { const request = {
get: <T>( get: <T>(url: string, config?: AxiosRequestConfig): Promise<RequestResult<T>> => {
url: string,
config?: Record<string, any>
): Promise<RequestResult<T>> => {
return axiosInstance.get(url, { ...config }); return axiosInstance.get(url, { ...config });
}, },
post: axiosInstance.post, // eslint-disable-next-line @typescript-eslint/no-explicit-any
post: <T, D = any>(
url: string,
data: D,
config?: AxiosRequestConfig
): Promise<RequestResult<T>> => {
return axiosInstance.post(url, data, { ...config });
},
put: axiosInstance.put, put: axiosInstance.put,
delete: axiosInstance.delete, delete: axiosInstance.delete,
}; };

View File

@@ -30,120 +30,118 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch } from "vue"; import { computed, watch } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { useDoubanStore } from "@/stores/douban"; import { useDoubanStore } from "@/stores/douban";
interface CurrentParams { interface CurrentParams {
type: string; type: string;
tag?: string; tag?: string;
} }
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const routeParams = computed( const routeParams = computed((): CurrentParams => ({ ...route.query }) as unknown as CurrentParams);
(): CurrentParams => ({ ...route.query }) as unknown as CurrentParams const doubanStore = useDoubanStore();
); if (routeParams.value) {
const doubanStore = useDoubanStore(); doubanStore.setCurrentParams(routeParams.value);
if (routeParams.value) { }
watch(
() => routeParams.value,
() => {
console.log(routeParams.value);
doubanStore.setCurrentParams(routeParams.value); doubanStore.setCurrentParams(routeParams.value);
} }
);
watch( const searchMovie = (title: string) => {
() => routeParams.value, router.push({ path: "/", query: { keyword: title } });
() => { };
console.log(routeParams.value);
doubanStore.setCurrentParams(routeParams.value);
}
);
const searchMovie = (title: string) => {
router.push({ path: "/", query: { keyword: title } });
};
</script> </script>
<style scoped> <style scoped>
.movie-wall { .movie-wall {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, 200px); grid-template-columns: repeat(auto-fill, 200px);
grid-row-gap: 15px; grid-row-gap: 15px;
justify-content: space-between; justify-content: space-between;
} }
.movie-item { .movie-item {
width: 200px; /* 设置固定宽度 */ width: 200px; /* 设置固定宽度 */
overflow: hidden; /* 确保内容不会超出卡片 */ overflow: hidden; /* 确保内容不会超出卡片 */
text-align: center; text-align: center;
background-color: #f9f9f9; /* 可选:设置背景颜色 */ background-color: #f9f9f9; /* 可选:设置背景颜色 */
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12); /* 可选:设置阴影效果 */ box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12); /* 可选:设置阴影效果 */
border-radius: 15px; /* 设置图片圆角 */ border-radius: 15px; /* 设置图片圆角 */
box-sizing: border-box; box-sizing: border-box;
padding-bottom: 0px; padding-bottom: 0px;
position: relative; position: relative;
padding: 15px; padding: 15px;
padding-bottom: 0; padding-bottom: 0;
} }
.movie-poster-img { .movie-poster-img {
width: 100%; width: 100%;
height: 220px; height: 220px;
object-fit: cover; /* 确保图片使用cover模式 */ object-fit: cover; /* 确保图片使用cover模式 */
border-radius: 15px; /* 设置图片圆角 */ border-radius: 15px; /* 设置图片圆角 */
overflow: hidden; overflow: hidden;
} }
.movie-info { .movie-info {
/* margin-top: 8px; */ /* margin-top: 8px; */
.movie-title { .movie-title {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
padding: 10px 0; padding: 10px 0;
}
}
.movie-poster {
width: 100%;
height: 220px;
position: relative;
overflow: hidden;
border-radius: 15px;
box-sizing: border-box;
padding: 0;
margin: 0;
overflow: hidden;
} }
}
.movie-poster {
width: 100%;
height: 220px;
position: relative;
overflow: hidden;
border-radius: 15px;
box-sizing: border-box;
padding: 0;
margin: 0;
overflow: hidden;
}
.movie-poster-hover { .movie-poster-hover {
opacity: 0; /* 默认情况下隐藏 */ opacity: 0; /* 默认情况下隐藏 */
transition: opacity 0.3s ease; /* 添加过渡效果 */ transition: opacity 0.3s ease; /* 添加过渡效果 */
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
/* height: 100%; */ /* height: 100%; */
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
} }
.movie-poster:hover .movie-poster-hover { .movie-poster:hover .movie-poster-hover {
opacity: 1; /* 鼠标移入时显示 */ opacity: 1; /* 鼠标移入时显示 */
} }
.movie-rate { .movie-rate {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 10px;
background-color: rgba(88, 83, 250, 0.8); background-color: rgba(88, 83, 250, 0.8);
color: white; color: white;
padding: 0px 8px; padding: 0px 8px;
border-radius: 5px; border-radius: 5px;
font-size: 14px; font-size: 14px;
} }
.movie-search { .movie-search {
color: white; color: white;
border-radius: 5px; border-radius: 5px;
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="home" v-loading="resourcStore.loading" element-loading-background="rgba(0,0,0,0.6)"> <div v-loading="resourcStore.loading" class="home" element-loading-background="rgba(0,0,0,0.6)">
<el-container> <el-container>
<el-aside width="200px"><aside-menu /></el-aside> <el-aside width="200px"><aside-menu /></el-aside>
<el-container class="home-main"> <el-container class="home-main">
@@ -32,62 +32,62 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
import { useStore } from "@/stores/index"; import { useStore } from "@/stores/index";
import { useUserSettingStore } from "@/stores/userSetting"; import { useUserSettingStore } from "@/stores/userSetting";
import { onUnmounted } from "vue"; import { onUnmounted } from "vue";
import AsideMenu from "@/components/AsideMenu.vue"; import AsideMenu from "@/components/AsideMenu.vue";
import SearchBar from "@/components/SearchBar.vue"; import SearchBar from "@/components/SearchBar.vue";
const resourcStore = useResourceStore(); const resourcStore = useResourceStore();
const store = useStore(); const store = useStore();
const settingStore = useUserSettingStore(); const settingStore = useUserSettingStore();
settingStore.getSettings(); settingStore.getSettings();
const handleScroll = () => { const handleScroll = () => {
const scrollTop = window.scrollY; const scrollTop = window.scrollY;
if (scrollTop > 50) { if (scrollTop > 50) {
store.scrollTop && store.setScrollTop(false); store.scrollTop && store.setScrollTop(false);
} else { } else {
!store.scrollTop && store.setScrollTop(true); !store.scrollTop && store.setScrollTop(true);
} }
}; };
window.addEventListener("scroll", handleScroll); window.addEventListener("scroll", handleScroll);
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("scroll", handleScroll); window.removeEventListener("scroll", handleScroll);
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.home { .home {
// padding: 20px; // padding: 20px;
min-width: 1000px; min-width: 1000px;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
margin: 0 auto; margin: 0 auto;
} }
.home-header { .home-header {
height: auto; height: auto;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
// padding: 0; // padding: 0;
background-color: rgba(231, 235, 239, 0.7) !important; background-color: rgba(231, 235, 239, 0.7) !important;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
// background-color: var(--theme-other_background); // background-color: var(--theme-other_background);
// box-shadow: 0 4px 10px rgba(225, 225, 225, 0.3); // box-shadow: 0 4px 10px rgba(225, 225, 225, 0.3);
// border-radius: 20px; // border-radius: 20px;
} }
.home-main { .home-main {
width: 1000px; width: 1000px;
height: 100vh; height: 100vh;
overflow: auto; overflow: auto;
} }
.home-main-main { .home-main-main {
padding: 10px 15px; padding: 10px 15px;
} }
.home-aside { .home-aside {
width: 300px; width: 300px;
} }
</style> </style>

View File

@@ -11,9 +11,9 @@
<!-- 登录表单 --> <!-- 登录表单 -->
<el-form <el-form
v-if="activeTab === 'login'" v-if="activeTab === 'login'"
ref="loginFormRef"
:model="loginForm" :model="loginForm"
:rules="loginRules" :rules="loginRules"
ref="loginFormRef"
label-position="top" label-position="top"
> >
<el-form-item prop="username" class="form-item"> <el-form-item prop="username" class="form-item">
@@ -46,9 +46,9 @@
<!-- 注册表单 --> <!-- 注册表单 -->
<el-form <el-form
v-if="activeTab === 'register'" v-if="activeTab === 'register'"
ref="registerFormRef"
:model="registerForm" :model="registerForm"
:rules="registerRules" :rules="registerRules"
ref="registerFormRef"
label-position="top" label-position="top"
><el-form-item prop="username" class="form-item"> ><el-form-item prop="username" class="form-item">
<el-input v-model="registerForm.username" placeholder="用户名" class="form-input" /> <el-input v-model="registerForm.username" placeholder="用户名" class="form-input" />
@@ -80,180 +80,181 @@
</template> </template>
<script setup type="ts"> <script setup type="ts">
import { ref } from "vue"; import { ref } from "vue";
import { userApi } from "@/api/user"; import { userApi } from "@/api/user";
import router from "@/router"; import router from "@/router";
import { ElMessage } from "element-plus";
const activeTab = ref("login"); // 默认显示登录表单 const activeTab = ref("login"); // 默认显示登录表单
const loginForm = ref({ const loginForm = ref({
username: "", username: "",
password: "", password: "",
}); });
const registerForm = ref({ const registerForm = ref({
username: "", username: "",
password: "", password: "",
registerCode: "", registerCode: "",
}); });
const password2 = ref(""); const password2 = ref("");
const loginRules = { const loginRules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }], username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }], password: [{ required: true, message: "请输入密码", trigger: "blur" }],
}; };
const registerRules = { const registerRules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }], username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }], password: [{ required: true, message: "请输入密码", trigger: "blur" }],
registerCode: [{ required: true, message: "请输入注册码", trigger: "blur" }], registerCode: [{ required: true, message: "请输入注册码", trigger: "blur" }],
}; };
const loginFormRef = ref(null); const loginFormRef = ref(null);
const registerFormRef = ref(null); const registerFormRef = ref(null);
const handleLogin = async () => { const handleLogin = async () => {
try { try {
const res = await userApi.login(loginForm.value); const res = await userApi.login(loginForm.value);
if (res.code === 0) { if (res.code === 0) {
const { token } = res.data; const { token } = res.data;
localStorage.setItem("token", token); localStorage.setItem("token", token);
// 路由跳转首页 // 路由跳转首页
router.push("/"); router.push("/");
} else { } else {
ElMessage.error(res.message); ElMessage.error(res.message);
}
} catch (error) {
ElMessage.error("登录失败", error);
} }
}; } catch (error) {
const loginFormRefValidate = () => { ElMessage.error("登录失败", error);
loginFormRef.value.validate((valid) => {
if (valid) {
handleLogin();
} else {
ElMessage.error("登录表单验证失败");
} }
}); };
}; const loginFormRefValidate = () => {
loginFormRef.value.validate((valid) => {
if (valid) {
handleLogin();
} else {
ElMessage.error("登录表单验证失败");
}
});
};
const handleRegister = async () => { const handleRegister = async () => {
registerFormRef.value.validate(async (valid) => { registerFormRef.value.validate(async (valid) => {
if (valid) { if (valid) {
if(password2.value !== registerForm.value.password){ if(password2.value !== registerForm.value.password){
return ElMessage.error("两次输入的密码不一致"); return ElMessage.error("两次输入的密码不一致");
}
try {
const res = await userApi.register(registerForm.value);
if (res.code === 0) {
ElMessage.success("注册成功");
loginForm.value.username = registerForm.value.username
loginForm.value.password = registerForm.value.password
handleLogin()
} else {
ElMessage.error(res.message || "注册失败");
}
} catch (error) {
ElMessage.error(error.message || "注册失败");
}
} else {
console.error("注册表单验证失败");
} }
}); try {
}; const res = await userApi.register(registerForm.value);
if (res.code === 0) {
ElMessage.success("注册成功");
loginForm.value.username = registerForm.value.username
loginForm.value.password = registerForm.value.password
handleLogin()
} else {
ElMessage.error(res.message || "注册失败");
}
} catch (error) {
ElMessage.error(error.message || "注册失败");
}
} else {
console.error("注册表单验证失败");
}
});
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.login-register { .login-register {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.login-bg { .login-bg {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: url("../assets/images/login-bg.jpg"); background-image: url("../assets/images/login-bg.jpg");
background-size: cover; background-size: cover;
background-position: center center; background-position: center center;
-webkit-filter: blur(3px); -webkit-filter: blur(3px);
-moz-filter: blur(3px); -moz-filter: blur(3px);
-o-filter: blur(3px); -o-filter: blur(3px);
-ms-filter: blur(3px); -ms-filter: blur(3px);
filter: blur(3px); filter: blur(3px);
z-index: 0; z-index: 0;
} }
.card { .card {
position: relative; position: relative;
z-index: 10; z-index: 10;
width: 480px; width: 480px;
border-radius: 16px; border-radius: 16px;
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
box-shadow: 0px 20px 60px rgba(123, 61, 224, 0.1); box-shadow: 0px 20px 60px rgba(123, 61, 224, 0.1);
} }
.tabs { .tabs {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-bottom: 20px; margin-bottom: 20px;
} }
.tab-button { .tab-button {
flex: 1; flex: 1;
text-align: center; text-align: center;
} }
.form-item { .form-item {
width: 100%; width: 100%;
margin-bottom: 30px; margin-bottom: 30px;
} }
.form-input { .form-input {
height: 48px; height: 48px;
border-radius: 10px; border-radius: 10px;
} }
.options { .options {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
} }
.forgot-password { .forgot-password {
color: #6366f1; color: #6366f1;
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: 14px;
} }
.form-submit { .form-submit {
margin-bottom: 10px; margin-bottom: 10px;
background-color: #6366f1; background-color: #6366f1;
width: 100%; width: 100%;
height: 48px; height: 48px;
} }
.google-login { .google-login {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
.agreement { .agreement {
text-align: center; text-align: center;
margin-top: 10px; margin-top: 10px;
font-size: 14px; font-size: 14px;
} }
.user-agreement { .user-agreement {
color: #6366f1; color: #6366f1;
text-decoration: none; text-decoration: none;
} }
</style> </style>

View File

@@ -11,8 +11,8 @@
</div> </div>
<div class="header_right"> <div class="header_right">
<el-icon <el-icon
class="type_icon"
v-if="userStore.displayStyle === 'card'" v-if="userStore.displayStyle === 'card'"
class="type_icon"
@click="setDisplayStyle('table')" @click="setDisplayStyle('table')"
><Menu ><Menu
/></el-icon> /></el-icon>
@@ -20,19 +20,23 @@
</div> </div>
</div> </div>
<ResourceTable <ResourceTable
@loadMore="handleLoadMore"
@searchMovieforTag="searchMovieforTag"
@save="handleSave"
v-if="userStore.displayStyle === 'table'" v-if="userStore.displayStyle === 'table'"
@load-more="handleLoadMore"
@search-moviefor-tag="searchMovieforTag"
@save="handleSave"
></ResourceTable> ></ResourceTable>
<ResourceCard <ResourceCard
@loadMore="handleLoadMore"
@searchMovieforTag="searchMovieforTag"
@save="handleSave"
v-else v-else
@load-more="handleLoadMore"
@search-moviefor-tag="searchMovieforTag"
@save="handleSave"
></ResourceCard> ></ResourceCard>
<el-empty v-if="resourceStore.resources.length === 0" :image-size="200" /> <el-empty v-if="resourceStore.resources.length === 0" :image-size="200" />
<el-dialog v-model="folderDialogVisible" title="选择保存目录" v-if="currentResource"> <el-dialog
v-if="currentResource"
v-model="saveDialogVisible"
:title="saveDialogMap[saveDialogStep].title"
>
<template #header="{ titleId }"> <template #header="{ titleId }">
<div class="my-header"> <div class="my-header">
<div :id="titleId"> <div :id="titleId">
@@ -43,144 +47,190 @@
> >
{{ currentResource.cloudType }} {{ currentResource.cloudType }}
</el-tag> </el-tag>
选择保存目录 {{ saveDialogMap[saveDialogStep].title }}
<span
v-if="resourceStore.shareInfo.fileSize && saveDialogStep === 1"
style="font-weight: bold"
>
({{ formattedFileSize(resourceStore.shareInfo.fileSize || 0) }})
</span>
</div> </div>
</div> </div>
</template> </template>
<folder-select <div v-loading="resourceStore.loadTree">
v-if="folderDialogVisible" <resource-select
@select="handleFolderSelect" v-if="saveDialogVisible && saveDialogStep === 1"
@close="folderDialogVisible = false" :cloud-type="currentResource.cloudType"
:cloudType="currentResource.cloudType" />
/> <folder-select
v-if="saveDialogVisible && saveDialogStep === 2"
:cloud-type="currentResource.cloudType"
@select="handleFolderSelect"
@close="saveDialogVisible = false"
/>
</div>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="folderDialogVisible = false">取消</el-button> <el-button @click="saveDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveBtnClick">保存</el-button> <el-button type="primary" @click="handleConfirmClick">{{
saveDialogMap[saveDialogStep].buttonText
}}</el-button>
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
import { useUserSettingStore } from "@/stores/userSetting"; import { useUserSettingStore } from "@/stores/userSetting";
import FolderSelect from "@/components/Home/FolderSelect.vue"; import FolderSelect from "@/components/Home/FolderSelect.vue";
import ResourceTable from "@/components/Home/ResourceTable.vue"; import ResourceSelect from "@/components/Home/ResourceSelect.vue";
import type { ResourceItem, TagColor } from "@/types"; import ResourceTable from "@/components/Home/ResourceTable.vue";
import ResourceCard from "@/components/Home/ResourceCard.vue"; import { formattedFileSize } from "@/utils/index";
import { useRouter } from "vue-router"; import type { ResourceItem, TagColor } from "@/types";
const router = useRouter();
const resourceStore = useResourceStore(); import ResourceCard from "@/components/Home/ResourceCard.vue";
const userStore = useUserSettingStore(); import { useRouter } from "vue-router";
const folderDialogVisible = ref(false); import { ElMessage } from "element-plus";
const currentResource = ref<ResourceItem | null>(null); const router = useRouter();
const currentFolderId = ref<string | null>(null);
const refreshResources = async () => { const resourceStore = useResourceStore();
resourceStore.searchResources("", false); const userStore = useUserSettingStore();
}; const saveDialogVisible = ref(false);
const currentResource = ref<ResourceItem | null>(null);
const currentFolderId = ref<string | null>(null);
const saveDialogStep = ref<1 | 2>(1);
const handleSave = (resource: ResourceItem) => { const refreshResources = async () => {
currentResource.value = resource; resourceStore.searchResources("", false);
folderDialogVisible.value = true; };
};
const handleFolderSelect = async (folderId: string) => { const saveDialogMap = {
if (!currentResource.value) return; 1: {
currentFolderId.value = folderId; title: "选择资源",
}; buttonText: "下一步",
},
2: {
title: "选择保存目录",
buttonText: "保存",
},
};
const handleSaveBtnClick = async () => { const handleSave = async (resource: ResourceItem) => {
if (!currentResource.value || !currentFolderId.value) return; currentResource.value = resource;
folderDialogVisible.value = false; saveDialogVisible.value = true;
await resourceStore.saveResource(currentResource.value, currentFolderId.value); saveDialogStep.value = 1;
}; if (!(await resourceStore.getResourceListAndSelect(currentResource.value))) {
const setDisplayStyle = (style: string) => { saveDialogVisible.value = false;
console.log(userStore); }
userStore.setDisplayStyle(style as "card" | "table"); };
};
// 添加加载更多处理函数
const handleLoadMore = (channelId: string) => {
resourceStore.searchResources("", true, channelId);
};
const searchMovieforTag = (tag: string) => { const handleFolderSelect = async (folderId: string) => {
console.log("iiii"); if (!currentResource.value) return;
router.push({ path: "/", query: { keyword: tag } }); currentFolderId.value = folderId;
}; };
const handleConfirmClick = async () => {
if (saveDialogStep.value === 1) {
if (resourceStore.resourceSelect.length === 0) {
ElMessage.warning("请选择要保存的资源");
return;
}
saveDialogStep.value = 2;
} else {
handleSaveBtnClick();
}
};
const handleSaveBtnClick = async () => {
if (!currentResource.value || !currentFolderId.value) return;
saveDialogVisible.value = false;
await resourceStore.saveResource(currentResource.value, currentFolderId.value);
};
const setDisplayStyle = (style: string) => {
console.log(userStore);
userStore.setDisplayStyle(style as "card" | "table");
};
// 添加加载更多处理函数
const handleLoadMore = (channelId: string) => {
resourceStore.searchResources("", true, channelId);
};
const searchMovieforTag = (tag: string) => {
router.push({ path: "/", query: { keyword: tag } });
};
</script> </script>
<style scoped> <style scoped>
.resource-list { .resource-list {
/* margin-top: 20px; */ /* margin-top: 20px; */
position: relative; position: relative;
} }
.resource-list__header { .resource-list__header {
height: 48px; height: 48px;
background-color: var(--theme-other_background); background-color: var(--theme-other_background);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 10px; margin-bottom: 10px;
box-sizing: border-box; box-sizing: border-box;
border-radius: 15px; border-radius: 15px;
padding: 0 15px; padding: 0 15px;
} }
.header_right { .header_right {
cursor: pointer; cursor: pointer;
} }
.type_icon { .type_icon {
width: 48px; width: 48px;
height: 48px; height: 48px;
size: 48px; size: 48px;
} }
.refresh_btn { .refresh_btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
.item-count {
color: #909399;
font-size: 0.9em;
}
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
.group-header {
display: flex;
align-items: center;
gap: 8px;
}
.item-count { .item-count {
color: #909399; color: #909399;
font-size: 0.9em; font-size: 0.9em;
} }
}
:deep(.el-table__expand-column) { .dialog-footer {
.cell { display: flex;
padding: 0 !important; align-items: center;
} justify-content: flex-end;
} }
:deep(.el-table__expanded-cell) { .group-header {
padding: 20px !important; display: flex;
} align-items: center;
gap: 8px;
}
:deep(.el-table__expand-icon) { .item-count {
height: 23px; color: #909399;
line-height: 23px; font-size: 0.9em;
} }
.load-more {
display: flex; :deep(.el-table__expand-column) {
justify-content: center; .cell {
padding: 16px 0; padding: 0 !important;
} }
}
:deep(.el-table__expanded-cell) {
padding: 20px !important;
}
:deep(.el-table__expand-icon) {
height: 23px;
line-height: 23px;
}
.load-more {
display: flex;
justify-content: center;
padding: 16px 0;
}
</style> </style>

View File

@@ -1,50 +1,50 @@
<template> <template>
<div class="settings"> <div class="settings">
<el-card class="setting-card" v-if="settingStore.globalSetting"> <el-card v-if="settingStore.globalSetting" class="setting-card">
<h2>网络配置</h2> <h2>网络配置</h2>
<div class="section"> <div class="section">
<div class="form-group"> <div class="form-group">
<label for="proxyDomain">代理域名:</label> <label for="proxyDomain">代理域名:</label>
<el-input <el-input
id="proxyDomain"
v-model="settingStore.globalSetting.httpProxyHost"
class="form-input" class="form-input"
type="text" type="text"
id="proxyDomain"
placeholder="127.0.0.1" placeholder="127.0.0.1"
v-model="settingStore.globalSetting.httpProxyHost"
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="proxyPort">代理端口:</label> <label for="proxyPort">代理端口:</label>
<el-input <el-input
id="proxyPort"
v-model="settingStore.globalSetting.httpProxyPort"
class="form-input" class="form-input"
type="text" type="text"
id="proxyPort"
placeholder="7890" placeholder="7890"
v-model="settingStore.globalSetting.httpProxyPort"
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="AdminUserCode">管理员注册码:</label> <label for="AdminUserCode">管理员注册码:</label>
<el-input-number <el-input-number
id="AdminUserCode"
v-model="settingStore.globalSetting.AdminUserCode"
class="form-input" class="form-input"
type="text" type="text"
id="AdminUserCode"
:controls="false" :controls="false"
:precision="0" :precision="0"
placeholder="设置管理员注册码" placeholder="设置管理员注册码"
v-model="settingStore.globalSetting.AdminUserCode"
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="CommonUserCode">普通用户注册码:</label> <label for="CommonUserCode">普通用户注册码:</label>
<el-input-number <el-input-number
id="CommonUserCode"
v-model="settingStore.globalSetting.CommonUserCode"
class="form-input" class="form-input"
type="text" type="text"
:precision="0" :precision="0"
:controls="false" :controls="false"
id="CommonUserCode"
placeholder="设置普通用户注册码" placeholder="设置普通用户注册码"
v-model="settingStore.globalSetting.CommonUserCode"
/> />
</div> </div>
</div> </div>
@@ -61,19 +61,19 @@
<div class="form-group"> <div class="form-group">
<label for="cookie115">115网盘Cookie:</label> <label for="cookie115">115网盘Cookie:</label>
<el-input <el-input
class="form-input"
type="text"
id="cookie115" id="cookie115"
v-model="settingStore.userSettings.cloud115Cookie" v-model="settingStore.userSettings.cloud115Cookie"
class="form-input"
type="text"
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="cookieQuark">夸克网盘Cookie:</label> <label for="cookieQuark">夸克网盘Cookie:</label>
<el-input <el-input
class="form-input"
type="text"
id="cookieQuark" id="cookieQuark"
v-model="settingStore.userSettings.quarkCookie" v-model="settingStore.userSettings.quarkCookie"
class="form-input"
type="text"
/> />
</div> </div>
</div> </div>
@@ -98,65 +98,65 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserSettingStore } from "@/stores/userSetting"; import { useUserSettingStore } from "@/stores/userSetting";
const settingStore = useUserSettingStore(); const settingStore = useUserSettingStore();
settingStore.getSettings(); settingStore.getSettings();
const saveSettings = () => { const saveSettings = () => {
settingStore.saveSettings(); settingStore.saveSettings();
// Add your save logic here // Add your save logic here
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.settings { .settings {
padding: 20px; padding: 20px;
} }
.setting-card { .setting-card {
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 15px; border-radius: 15px;
} }
.section { .section {
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
} }
.form-group { .form-group {
margin-bottom: 10px; margin-bottom: 10px;
width: 48%; width: 48%;
} }
.form-input { .form-input {
text-align: left; text-align: left;
width: 100%; width: 100%;
} }
::v-deep .el-input__inner { ::v-deep .el-input__inner {
text-align: left; text-align: left;
} }
label { label {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
} }
input { input {
width: 100%; width: 100%;
padding: 8px; padding: 8px;
box-sizing: border-box; box-sizing: border-box;
} }
button { button {
padding: 10px 20px; padding: 10px 20px;
background-color: #007bff; background-color: #007bff;
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
} }
button:hover { button:hover {
background-color: #0056b3; background-color: #0056b3;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
{ {
"name": "cloud-saver", "name": "cloud-saver",
"version": "0.1.0", "version": "0.1.2",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"frontend", "frontend",
@@ -17,13 +17,23 @@
"build:frontend": "cd frontend && npm run build", "build:frontend": "cd frontend && npm run build",
"build:backend": "cd backend && npm run build", "build:backend": "cd backend && npm run build",
"clean": "rimraf **/node_modules **/dist", "clean": "rimraf **/node_modules **/dist",
"format": "prettier --write \"**/*.{js,ts,vue,json,md}\"", "format": "prettier --write \"**/*.{js,ts,vue,json,css,scss}\"",
"format:check": "prettier --check \"**/*.{js,ts,vue,json,md}\"" "format:check": "prettier --check \"**/*.{js,ts,vue,json,css,scss}\"",
"format:all": "npm run format && npm run lint:fix",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.32.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.2.5", "prettier": "^3.4.2",
"rimraf": "^5.0.5" "rimraf": "^5.0.5",
"vue-eslint-parser": "^9.4.3"
}, },
"engines": { "engines": {
"pnpm": ">=6.0.0" "pnpm": ">=6.0.0"

837
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff