19 Commits

Author SHA1 Message Date
jiangrui
c1948a888e Merge remote-tracking branch 'origin/dev' 2025-03-09 23:10:27 +08:00
jiangrui1994
7d8f4b32f7 更新 README.md 2025-03-09 23:08:37 +08:00
jiangrui
0a9b053dff Optimized image loading 2025-03-08 20:04:57 +08:00
jiangrui
73440cca45 fix:mobile scroll to more 2025-03-08 18:26:39 +08:00
jiangrui
5987e1fb3e caiyun RegExp 2025-03-08 15:16:57 +08:00
jiangrui
42722ca1d8 Update the version number 2025-03-08 12:39:15 +08:00
jiangrui
6efdea55aa Optimized the PC experience 2025-03-08 12:33:42 +08:00
jiangrui
f23e78e8dd refactor:优化展开收起 2025-03-07 23:48:48 +08:00
jiangrui
26381fa6b0 fix:优化触底逻辑 2025-03-07 23:24:28 +08:00
jiangrui
eedd68d137 fix:优化搜索逻辑 2025-03-07 23:21:24 +08:00
jiangrui
a04f16bfa4 修改123网盘匹配正则 2025-03-07 16:52:14 +08:00
jiangrui
93d6a1276a update 123 RegExp 2025-03-07 14:11:52 +08:00
jiangrui
505b1d6c67 Update README.md 2025-03-06 22:42:08 +08:00
jiangrui
212ca3a879 Update README.md 2025-03-06 22:35:34 +08:00
jiangrui
65a35225d0 build:update actions 2025-03-06 22:13:56 +08:00
jiangrui
7ed0c04111 build:修改启动命令 2025-03-06 21:30:44 +08:00
jiangrui
e8f70b286e build:修改docker构建配置与简化后端config 2025-03-06 18:24:01 +08:00
jiangrui
e708524a41 build:完善构建流程 2025-03-06 16:41:54 +08:00
jiangrui
a01dd06ef2 fix:解决ios设备兼容问题 2025-03-06 16:40:43 +08:00
26 changed files with 315 additions and 129 deletions

View File

@@ -1,8 +0,0 @@
# jwt密钥 用于生成token加密
JWT_SECRET=""
# 用户注册码
REGISTER_CODE='9527'
# 服务端口
PORT=8009

View File

@@ -1,8 +1,5 @@
name: Build and Push Multi-Arch Docker Image for Test name: Build and Push Multi-Arch Docker Image for Test
on: on:
push:
branches:
- dev
workflow_dispatch: # 添加手动触发 workflow_dispatch: # 添加手动触发
jobs: jobs:
build-and-push: build-and-push:
@@ -12,6 +9,8 @@ jobs:
packages: write # 必须授权以推送镜像 packages: write # 必须授权以推送镜像
env: env:
REPO_NAME: ${{ github.repository }} REPO_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
IMAGE_NAME: cloudsaver
steps: steps:
- name: 检出代码 - name: 检出代码
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -28,6 +27,12 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: 登录到 Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 设置 QEMU 支持多架构 - name: 设置 QEMU 支持多架构
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
@@ -42,3 +47,4 @@ jobs:
push: true push: true
tags: | tags: |
ghcr.io/${{ env.LOWER_NAME }}:test ghcr.io/${{ env.LOWER_NAME }}:test
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:test

View File

@@ -1,7 +1,7 @@
name: Docker Image CI/CD name: Docker Image CI/CD
on: on:
push: push:
tags: [ "v*.*.*" ] # 支持标签触发(如 v1.0.0 tags: ["v*.*.*"] # 支持标签触发(如 v1.0.0
workflow_dispatch: # 添加手动触发 workflow_dispatch: # 添加手动触发
jobs: jobs:
build-and-push: build-and-push:
@@ -11,14 +11,18 @@ jobs:
packages: write # 必须授权以推送镜像 packages: write # 必须授权以推送镜像
env: env:
REPO_NAME: ${{ github.repository }} REPO_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
IMAGE_NAME: cloudsaver
steps: steps:
- name: 检出代码 - name: 检出代码
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 设置小写镜像名称 - name: 设置小写镜像名称和版本
run: | run: |
LOWER_NAME=$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]') LOWER_NAME=$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')
echo "LOWER_NAME=$LOWER_NAME" >> $GITHUB_ENV echo "LOWER_NAME=$LOWER_NAME" >> $GITHUB_ENV
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: 登录到 GitHub Container Registry - name: 登录到 GitHub Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
@@ -27,6 +31,12 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: 登录到 Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 设置 QEMU 支持多架构 - name: 设置 QEMU 支持多架构
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
@@ -41,4 +51,6 @@ jobs:
push: true push: true
tags: | tags: |
ghcr.io/${{ env.LOWER_NAME }}:latest ghcr.io/${{ env.LOWER_NAME }}:latest
ghcr.io/${{ env.LOWER_NAME }}:${{ github.sha }} ghcr.io/${{ env.LOWER_NAME }}:${{ env.VERSION }}
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}

View File

@@ -26,6 +26,9 @@ RUN apk add --no-cache nginx
# 设置工作目录 # 设置工作目录
WORKDIR /app WORKDIR /app
# 创建配置和数据目录
RUN mkdir -p /app/config /app/data
# 复制前端构建产物到 Nginx # 复制前端构建产物到 Nginx
COPY --from=frontend-build /app/dist /usr/share/nginx/html COPY --from=frontend-build /app/dist /usr/share/nginx/html
@@ -38,8 +41,15 @@ COPY --from=backend-build /app /app
# 安装生产环境依赖 # 安装生产环境依赖
RUN npm install --production RUN npm install --production
# 设置数据卷
VOLUME ["/app/config", "/app/data"]
# 暴露端口 # 暴露端口
EXPOSE 8008 EXPOSE 8008
# 启动 Nginx 和后端服务 # 启动脚本
CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start && wait"] COPY docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh
# 启动服务
ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@@ -45,7 +45,6 @@
<p>资源转存</p> <p>资源转存</p>
</div> </div>
### 移动端 ### 移动端
<div align="center"> <div align="center">
@@ -108,7 +107,7 @@ pnpm install
3. 配置环境变量 3. 配置环境变量
```bash ```bash
cp .env.example ./backend/.env cp ./backend/.env.example ./backend/.env
``` ```
根据 `.env.example` 文件说明配置必要的环境变量。 根据 `.env.example` 文件说明配置必要的环境变量。
@@ -142,6 +141,15 @@ pnpm start
### Docker 部署 ### Docker 部署
说明:镜像源有**两个地址**供选择下面部署命令中使用的是dockerhub托管的地址为例github托管的地址请自行替换
- dockerhub托管
- `jiangrui1994/cloudsaver:latest` 稳定版
- `jiangrui1994/cloudsaver:test` 测试版 包含最新功能和bug修复但可能不如稳定版稳定
- github托管
- `ghcr.io/jiangrui1994/cloudsaver:latest` 稳定版
- `ghcr.io/jiangrui1994/cloudsaver:test` 测试版 包含最新功能和bug修复但可能不如稳定版稳定
#### 单容器部署 #### 单容器部署
稳定版: 稳定版:
@@ -150,8 +158,9 @@ pnpm start
docker run -d \ docker run -d \
-p 8008:8008 \ -p 8008:8008 \
-v /your/local/path:/app/data \ -v /your/local/path:/app/data \
-v /your/local/path/config:/app/config \
--name cloud-saver \ --name cloud-saver \
ghcr.io/jiangrui1994/cloudsaver:latest jiangrui1994/cloudsaver:latest
``` ```
测试版包含最新功能和bug修复但可能不如稳定版稳定 测试版包含最新功能和bug修复但可能不如稳定版稳定
@@ -160,8 +169,9 @@ docker run -d \
docker run -d \ docker run -d \
-p 8008:8008 \ -p 8008:8008 \
-v /your/local/path:/app/data \ -v /your/local/path:/app/data \
-v /your/local/path/config:/app/config \
--name cloud-saver \ --name cloud-saver \
ghcr.io/jiangrui1994/cloudsaver:test jiangrui1994/cloudsaver:test
``` ```
#### Docker Compose 部署 #### Docker Compose 部署
@@ -174,12 +184,13 @@ docker run -d \
version: "3" version: "3"
services: services:
cloudsaver: cloudsaver:
image: ghcr.io/jiangrui1994/cloudsaver:latest image: jiangrui1994/cloudsaver:latest
container_name: cloud-saver container_name: cloud-saver
ports: ports:
- "8008:8008" - "8008:8008"
volumes: volumes:
- /your/local/path:/app/data - /your/local/path:/app/data
- /your/local/path/config:/app/config
restart: unless-stopped restart: unless-stopped
``` ```
@@ -189,15 +200,31 @@ services:
version: "3" version: "3"
services: services:
cloudsaver: cloudsaver:
image: ghcr.io/jiangrui1994/cloudsaver:test image: jiangrui1994/cloudsaver:test
container_name: cloud-saver container_name: cloud-saver
ports: ports:
- "8008:8008" - "8008:8008"
volumes: volumes:
- /your/local/path:/app/data - /your/local/path:/app/data
- /your/local/path/config:/app/config
restart: unless-stopped restart: unless-stopped
``` ```
#### /app/config 目录说明
- `env` 文件:包含后端环境变量配置
```bash
# JWT配置
JWT_SECRET=your_jwt_secret_here
# Telegram配置
TELEGRAM_BASE_URL=https://t.me/s
# Telegram频道配置
TELE_CHANNELS=[{"id":"xxxx","name":"xxxx资源分享"}]
```
运行: 运行:
```bash ```bash
@@ -230,7 +257,7 @@ docker-compose up -d
- ⭐ 给项目点个 Star - ⭐ 给项目点个 Star
- 🎉 分享给更多有需要的朋友 - 🎉 分享给更多有需要的朋友
- ☕ 请作者喝咖啡 - ☕ 请作者喝瓶可乐🥤或者咖啡
<div align="center"> <div align="center">
<div style="display: inline-block; margin: 0 20px;"> <div style="display: inline-block; margin: 0 20px;">
@@ -258,7 +285,6 @@ docker-compose up -d
本项目基于 MIT 协议开源 - 查看 [LICENSE](LICENSE) 文件了解更多细节 本项目基于 MIT 协议开源 - 查看 [LICENSE](LICENSE) 文件了解更多细节
## 鸣谢 ## 鸣谢
- 👨‍💻 感谢所有为这个项目做出贡献的开发者们! - 👨‍💻 感谢所有为这个项目做出贡献的开发者们!

9
backend/.env.example Normal file
View File

@@ -0,0 +1,9 @@
# JWT配置
JWT_SECRET=your_jwt_secret_here
# Telegram配置
TELEGRAM_BASE_URL=https://t.me/s
# Telegram频道配置
TELE_CHANNELS=[{"id":"guaguale115","name":"115网盘资源分享"},{"id":"hao115","name":"115网盘资源分享频道"},{"id":"yunpanshare","name":"网盘资源收藏(夸克)"}]

View File

@@ -11,45 +11,35 @@ interface Channel {
interface CloudPatterns { interface CloudPatterns {
baiduPan: RegExp; baiduPan: RegExp;
tianyi: RegExp; tianyi: RegExp;
weiyun: RegExp;
aliyun: RegExp; aliyun: RegExp;
pan115: RegExp; pan115: RegExp;
pan123: RegExp;
quark: RegExp; quark: RegExp;
yidong: RegExp;
} }
interface Cloud115Config {
userId: string;
cookie: string;
}
interface QuarkConfig {
userId: string;
cookie: string;
}
interface HttpProxyConfig {
host: string;
port: string;
}
interface Config { interface Config {
jwtSecret: string; jwtSecret: string;
registerCode: string; telegram: {
rss: {
baseUrl: string; baseUrl: string;
channels: Channel[]; channels: Channel[];
}; };
telegram: {
baseUrl: string;
};
httpProxy: HttpProxyConfig;
cloudPatterns: CloudPatterns; cloudPatterns: CloudPatterns;
cloud115: Cloud115Config;
quark: QuarkConfig;
} }
export const config: Config = { // 从环境变量读取频道配置
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!", const getTeleChannels = (): Channel[] => {
rss: { try {
baseUrl: process.env.RSS_BASE_URL || "https://rsshub.rssforever.com/telegram/channel", const channelsStr = process.env.TELE_CHANNELS;
channels: [ if (channelsStr) {
return JSON.parse(channelsStr);
}
} catch (error) {
console.warn("无法解析 TELE_CHANNELS 环境变量,使用默认配置");
}
// 默认配置
return [
{ {
id: "guaguale115", id: "guaguale115",
name: "115网盘资源分享", name: "115网盘资源分享",
@@ -62,35 +52,26 @@ export const config: Config = {
id: "yunpanshare", id: "yunpanshare",
name: "网盘资源收藏(夸克)", name: "网盘资源收藏(夸克)",
}, },
], ];
}, };
registerCode: process.env.REGISTER_CODE || "9527",
export const config: Config = {
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!",
telegram: { telegram: {
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s", baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
}, channels: getTeleChannels(),
httpProxy: {
host: process.env.HTTP_PROXY_HOST || "",
port: process.env.HTTP_PROXY_PORT || "",
}, },
cloudPatterns: { cloudPatterns: {
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g, baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g, tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
weiyun: /https?:\/\/share\.weiyun\.com\/[^\s<>"]+/g, aliyun: /https?:\/\/\w+\.(?:alipan|aliyundrive)\.com\/[^\s<>"]+/g,
aliyun: /https?:\/\/\w+\.aliyundrive\.com\/[^\s<>"]+/g, // pan115有两个域名 115.com 和 anxia.com 和 115cdn.com
// pan115有两个域名 115.com 和 anxia.com
pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g, pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g,
// 修改为匹配所有以123开头的域名
pan123: /https?:\/\/(?:www\.)?123[^\/\s<>"]+\.com\/s\/[^\s<>"]+/g,
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g, quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
}, yidong: /https?:\/\/caiyun\.139\.com\/[^\s<>"]+/g,
cloud115: {
userId: "",
cookie: process.env.CLOUD115_COOKIE || "",
},
quark: {
userId: process.env.QUARK_USER_ID || "",
cookie: process.env.QUARK_COOKIE || "",
}, },
}; };

View File

@@ -1,7 +1,6 @@
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
import { createAxiosInstance } from "../utils/axiosInstance"; import { createAxiosInstance } from "../utils/axiosInstance";
import { Logger } from "../utils/logger"; import { Logger } from "../utils/logger";
import { config } from "../config/index";
import { ShareInfoResponse } from "../types/cloud115"; import { ShareInfoResponse } from "../types/cloud115";
interface Cloud115ListItem { interface Cloud115ListItem {
@@ -128,7 +127,6 @@ export class Cloud115Service {
}): Promise<{ message: string; data: unknown }> { }): Promise<{ message: string; data: unknown }> {
const param = new URLSearchParams({ const param = new URLSearchParams({
cid: params.cid, cid: params.cid,
user_id: config.cloud115.userId,
share_code: params.shareCode, share_code: params.shareCode,
receive_code: params.receiveCode, receive_code: params.receiveCode,
file_id: params.fileId, file_id: params.fileId,

View File

@@ -80,8 +80,8 @@ export class Searcher {
const allResults: any[] = []; const allResults: any[] = [];
const channelList: any[] = channelId const channelList: any[] = channelId
? config.rss.channels.filter((channel: any) => channel.id === channelId) ? config.telegram.channels.filter((channel: any) => channel.id === channelId)
: config.rss.channels; : config.telegram.channels;
// 使用Promise.all进行并行请求 // 使用Promise.all进行并行请求
const searchPromises = channelList.map(async (channel) => { const searchPromises = channelList.map(async (channel) => {

13
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# 如果配置目录下没有 env 文件,则复制示例文件
if [ ! -f /app/config/env ]; then
cp /app/.env.example /app/config/env
echo "已创建默认配置文件 /app/config/env请根据需要修改配置"
fi
# 创建配置文件软链接
ln -sf /app/config/env /app/.env
# 启动 Nginx 和后端服务
nginx -g 'daemon off;' & npm run start

View File

@@ -1,12 +1,12 @@
{ {
"name": "cloud-disk-web", "name": "cloud-disk-web",
"version": "0.1.0", "version": "0.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cloud-disk-web", "name": "cloud-disk-web",
"version": "0.1.0", "version": "0.2.3",
"dependencies": { "dependencies": {
"axios": "^1.6.7", "axios": "^1.6.7",
"element-plus": "^2.6.1", "element-plus": "^2.6.1",

View File

@@ -1,7 +1,7 @@
{ {
"name": "cloud-saver-web", "name": "cloud-saver-web",
"private": true, "private": true,
"version": "0.2.1", "version": "0.2.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",

View File

@@ -7,6 +7,10 @@
<style> <style>
#app { #app {
height: 100vh; height: 100vh;
width: 100%;
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
} }
:root { :root {
--theme-color: #3e3e3e; --theme-color: #3e3e3e;
@@ -34,6 +38,13 @@ body {
word-wrap: break-word; word-wrap: break-word;
} }
body {
position: fixed;
width: 100%;
height: 100%;
overflow: hidden;
}
/* 移动端全局样式 */ /* 移动端全局样式 */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
#app { #app {

View File

@@ -12,7 +12,11 @@
<div class="detail-cover"> <div class="detail-cover">
<el-image <el-image
class="cover-image" class="cover-image"
:src="`/tele-images/?url=${encodeURIComponent(currentResource.image as string)}`" :src="
userStore.imagesSource === 'proxy'
? `/tele-images/?url=${encodeURIComponent(currentResource.image as string)}`
: currentResource.image
"
fit="cover" fit="cover"
/> />
<el-tag <el-tag
@@ -56,14 +60,25 @@
</el-dialog> </el-dialog>
<div v-for="group in store.resources" :key="group.id" class="resource-group"> <div v-for="group in store.resources" :key="group.id" class="resource-group">
<div class="group-header"> <div class="group-header" @click="group.displayList = !group.displayList">
<el-link <el-link
class="group-title" class="group-title"
:href="`https://t.me/s/${group.id}`" :href="`https://t.me/s/${group.id}`"
target="_blank" target="_blank"
:underline="false" :underline="false"
@click.stop
> >
<el-image :src="group.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy /> <el-image
:src="
userStore.imagesSource === 'proxy'
? `/tele-images/?url=${encodeURIComponent(group.channelInfo.channelLogo)}`
: group.channelInfo.channelLogo
"
class="channel-logo"
scroll-container="#pc-resources-content"
fit="cover"
loading="lazy"
/>
<span>{{ group.channelInfo.name }}</span> <span>{{ group.channelInfo.name }}</span>
<span class="item-count">({{ group.list.length }})</span> <span class="item-count">({{ group.list.length }})</span>
</el-link> </el-link>
@@ -88,10 +103,14 @@
<div class="card-wrapper"> <div class="card-wrapper">
<div class="card-cover"> <div class="card-cover">
<el-image <el-image
loading="lazy"
class="cover-image" class="cover-image"
:src="`/tele-images/?url=${encodeURIComponent(resource.image as string)}`" :src="
userStore.imagesSource === 'proxy'
? `/tele-images/?url=${encodeURIComponent(resource.image as string)}`
: resource.image
"
fit="cover" fit="cover"
lazy
:alt="resource.title" :alt="resource.title"
@click="showResourceDetail(resource)" @click="showResourceDetail(resource)"
/> />
@@ -160,8 +179,11 @@ import { useResourceStore } from "@/stores/resource";
import { ref } from "vue"; import { ref } from "vue";
import type { ResourceItem, TagColor } from "@/types"; import type { ResourceItem, TagColor } from "@/types";
import { ArrowDown, Plus } from "@element-plus/icons-vue"; import { ArrowDown, Plus } from "@element-plus/icons-vue";
import { useUserSettingStore } from "@/stores/userSetting";
const userStore = useUserSettingStore();
const store = useResourceStore(); const store = useResourceStore();
const showDetail = ref(false); const showDetail = ref(false);
const currentResource = ref<ResourceItem | null>(null); const currentResource = ref<ResourceItem | null>(null);
@@ -216,6 +238,15 @@ const handleLoadMore = (channelId: string) => {
justify-content: space-between; justify-content: space-between;
padding: 12px 20px; padding: 12px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06); border-bottom: 1px solid rgba(0, 0, 0, 0.06);
position: sticky;
top: 0;
background: var(--theme-card-bg);
backdrop-filter: var(--theme-blur);
-webkit-backdrop-filter: var(--theme-blur);
z-index: 10;
border-radius: var(--theme-radius) var(--theme-radius) 0 0;
overflow: hidden;
cursor: pointer;
.group-title { .group-title {
@include flex-center; @include flex-center;
@@ -230,6 +261,7 @@ const handleLoadMore = (channelId: string) => {
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
box-shadow: var(--theme-shadow-sm); box-shadow: var(--theme-shadow-sm);
margin-right: 8px;
} }
.item-count { .item-count {

View File

@@ -15,10 +15,18 @@
<el-image <el-image
v-if="row.image" v-if="row.image"
class="table-item-image" class="table-item-image"
:src="`/tele-images/?url=${encodeURIComponent(row.image as string)}`" :src="
userStore.imagesSource === 'proxy'
? `/tele-images/?url=${encodeURIComponent(row.image as string)}`
: row.image
"
hide-on-click-modal hide-on-click-modal
:preview-src-list="[ :preview-src-list="[
`${location.origin}/tele-images/?url=${encodeURIComponent(row.image as string)}`, `${location.origin}${
userStore.imagesSource === 'proxy'
? '/tele-images/?url=' + encodeURIComponent(row.image as string)
: row.image
}`,
]" ]"
:zoom-rate="1.2" :zoom-rate="1.2"
:max-scale="7" :max-scale="7"
@@ -84,7 +92,16 @@
<el-table-column label="来源" prop="channel"> <el-table-column label="来源" prop="channel">
<template #default="{ row }"> <template #default="{ row }">
<div class="group-header"> <div class="group-header">
<el-image :src="row.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy /> <el-image
:src="
userStore.imagesSource === 'proxy'
? `/tele-images/?url=${encodeURIComponent(row.channelInfo.channelLogo as string)}`
: row.channelInfo.channelLogo
"
class="channel-logo"
fit="cover"
lazy
/>
<span>{{ row.channelInfo.name }}</span> <span>{{ row.channelInfo.name }}</span>
<span class="item-count">({{ row.list.length }})</span> <span class="item-count">({{ row.list.length }})</span>
</div> </div>
@@ -97,6 +114,8 @@
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";
import { useUserSettingStore } from "@/stores/userSetting";
const userStore = useUserSettingStore();
const store = useResourceStore(); const store = useResourceStore();

View File

@@ -83,10 +83,16 @@ watch(
keyword.value = newKeyword; keyword.value = newKeyword;
handleSearch(); handleSearch();
} else { } else {
keyword.value = ""; keyword.value = resourcStore.keyword;
} }
} }
); );
watch(
() => resourcStore.keyword,
(newKeyword) => {
keyword.value = newKeyword;
}
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -26,8 +26,10 @@
<!-- 描述 - 添加展开收起功能 --> <!-- 描述 - 添加展开收起功能 -->
<div <div
class="info__desc" class="info__desc"
:class="{ 'is-expanded': expandedItems[item.id] }" :class="{
@click="toggleExpand(item.id)" 'is-expanded': expandedItems[(item.messageId || '') + (item.channelId || '')],
}"
@click="toggleExpand((item.messageId || '') + (item.channelId || ''))"
v-html="item.content" v-html="item.content"
/> />

View File

@@ -100,12 +100,12 @@ export const useResourceStore = defineStore("resource", {
pan115: "danger", pan115: "danger",
quark: "success", quark: "success",
}, },
keyword: "",
resources: lastResource.list, resources: lastResource.list,
lastUpdateTime: lastResource.lastUpdateTime || "", lastUpdateTime: lastResource.lastUpdateTime || "",
shareInfo: {} as ShareInfoResponse, shareInfo: {} as ShareInfoResponse,
resourceSelect: [] as ShareInfo[], resourceSelect: [] as ShareInfo[],
loading: false, loading: false,
lastKeyWord: "",
backupPlan: false, backupPlan: false,
loadTree: false, loadTree: false,
}), }),
@@ -123,36 +123,45 @@ export const useResourceStore = defineStore("resource", {
if (isLoadMore) { if (isLoadMore) {
const list = this.resources.find((x) => x.id === channelId)?.list || []; const list = this.resources.find((x) => x.id === channelId)?.list || [];
lastMessageId = list[list.length - 1].messageId || ""; lastMessageId = list[list.length - 1].messageId || "";
if (list[list.length - 1].isLastMessage) {
ElMessage.warning("没有更多了~");
return;
}
if (!lastMessageId) { if (!lastMessageId) {
ElMessage.error("当次搜索源不支持加载更多"); ElMessage.error("当次搜索源不支持加载更多");
return; return;
} }
keyword = this.lastKeyWord; keyword = this.keyword;
} }
let { data = [] } = await resourceApi.search(keyword || "", channelId, lastMessageId); let { data = [] } = await resourceApi.search(keyword || "", channelId, lastMessageId);
this.keyword = keyword || "";
data = data.filter((item) => item.list.length > 0); data = data.filter((item) => item.list.length > 0);
this.lastKeyWord = keyword || "";
if (isLoadMore) { if (isLoadMore) {
const findedIndex = this.resources.findIndex((item) => item.id === data[0]?.id); const findedIndex = this.resources.findIndex((item) => item.id === data[0]?.id);
if (findedIndex !== -1) { if (findedIndex !== -1) {
this.resources[findedIndex].list.push(...data[0].list); this.resources[findedIndex].list.push(...data[0].list);
} }
if (data.length === 0) { if (data.length === 0) {
const list = this.resources.find((item) => item.id === channelId)?.list;
list && list[list.length - 1] && (list[list.length - 1]!.isLastMessage = true);
ElMessage.warning("没有更多了~"); ElMessage.warning("没有更多了~");
} }
} else { } else {
this.resources = data.map((item) => ({ ...item, displayList: true })); this.resources = data.map((item, index) => ({ ...item, displayList: index === 0 }));
if (this.resources.length === 0) { if (!keyword) {
ElMessage.warning("未搜索到相关资源");
}
}
// 获取当前时间字符串 用于存储到本地 // 获取当前时间字符串 用于存储到本地
this.lastUpdateTime = new Date().toLocaleString(); this.lastUpdateTime = new Date().toLocaleString();
localStorage.setItem( localStorage.setItem(
"last_resource_list", "last_resource_list",
JSON.stringify({ list: this.resources, lastUpdateTime: this.lastUpdateTime }) JSON.stringify({ list: this.resources, lastUpdateTime: this.lastUpdateTime })
); );
}
if (this.resources.length === 0) {
ElMessage.warning("未搜索到相关资源");
}
}
} catch (error) { } catch (error) {
console.log(error);
this.handleError("搜索失败,请重试", null); this.handleError("搜索失败,请重试", null);
} finally { } finally {
this.loading = false; this.loading = false;

View File

@@ -14,7 +14,8 @@ export const useUserSettingStore = defineStore("user", {
cloud115Cookie: "", cloud115Cookie: "",
quarkCookie: "", quarkCookie: "",
}, },
displayStyle: "card", displayStyle: (localStorage.getItem("display_style") as "table" | "card") || "card",
imagesSource: (localStorage.getItem("images_source") as "proxy" | "local") || "proxy",
}), }),
actions: { actions: {
@@ -41,7 +42,14 @@ export const useUserSettingStore = defineStore("user", {
setDisplayStyle(style: "table" | "card") { setDisplayStyle(style: "table" | "card") {
this.displayStyle = style; this.displayStyle = style;
ElMessage.success(`切换成功,当前为${style}模式`); localStorage.setItem("display_style", style);
ElMessage.success(`切换成功,当前为${style === "table" ? "列表" : "卡片"}模式`);
},
setImagesSource(source: "proxy" | "local") {
this.imagesSource = source;
localStorage.setItem("images_source", source);
ElMessage.success(`切换成功,图片模式当前为${source === "proxy" ? "代理" : "直连"}模式`);
}, },
}, },
}); });

View File

@@ -10,6 +10,7 @@ export interface ResourceItem {
pubDate: string; pubDate: string;
cloudType: string; cloudType: string;
messageId?: string; messageId?: string;
isLastMessage?: boolean;
} }
export interface Resource { export interface Resource {

View File

@@ -15,4 +15,5 @@ export interface UserSettingStore {
globalSetting: GlobalSettingAttributes | null; globalSetting: GlobalSettingAttributes | null;
userSettings: UserSettingAttributes; userSettings: UserSettingAttributes;
displayStyle: "table" | "card"; displayStyle: "table" | "card";
imagesSource: "proxy" | "local";
} }

View File

@@ -13,6 +13,25 @@
</div> </div>
<div class="header__right"> <div class="header__right">
<el-tooltip
effect="dark"
:content="
userStore.imagesSource === 'local' ? '图片切换到代理模式' : '图片切换到直连模式'
"
placement="bottom"
>
<el-button
type="text"
class="view-toggle"
@click="
userStore.setImagesSource(userStore.imagesSource === 'proxy' ? 'local' : 'proxy')
"
>
<el-icon>
<component :is="userStore.imagesSource === 'proxy' ? 'Guide' : 'Location'" />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip <el-tooltip
effect="dark" effect="dark"
:content="userStore.displayStyle === 'card' ? '切换到列表视图' : '切换到卡片视图'" :content="userStore.displayStyle === 'card' ? '切换到列表视图' : '切换到卡片视图'"
@@ -32,7 +51,7 @@
</div> </div>
<!-- 资源列表 --> <!-- 资源列表 -->
<div ref="contentRef" class="pc-resources__content"> <div id="pc-resources-content" ref="contentRef" class="pc-resources__content">
<component <component
:is="userStore.displayStyle === 'table' ? ResourceTable : ResourceCard" :is="userStore.displayStyle === 'table' ? ResourceTable : ResourceCard"
v-if="resourceStore.resources.length > 0" v-if="resourceStore.resources.length > 0"
@@ -142,7 +161,7 @@ import ResourceSelect from "@/components/Home/ResourceSelect.vue";
import ResourceTable from "@/components/Home/ResourceTable.vue"; import ResourceTable from "@/components/Home/ResourceTable.vue";
import { formattedFileSize } from "@/utils/index"; import { formattedFileSize } from "@/utils/index";
import type { ResourceItem, TagColor } from "@/types"; import type { ResourceItem, TagColor } from "@/types";
import { onMounted, onBeforeUnmount } from "vue";
import ResourceCard from "@/components/Home/ResourceCard.vue"; import ResourceCard from "@/components/Home/ResourceCard.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
@@ -212,8 +231,20 @@ const handleLoadMore = (channelId: string) => {
}; };
const searchMovieforTag = (tag: string) => { const searchMovieforTag = (tag: string) => {
router.push({ path: "/", query: { keyword: tag } }); router.push({ path: "/resource", query: { keyword: tag } });
}; };
// 页面进入 设置缓存的数据源
onMounted(() => {
const lastResourceList = localStorage.getItem("last_resource_list");
if (lastResourceList) {
resourceStore.resources = JSON.parse(lastResourceList).list;
}
});
// 页面销毁 清除搜索词
onBeforeUnmount(() => {
resourceStore.keyword = "";
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -81,10 +81,16 @@ watch(
searchForm.value.keyword = keyword; searchForm.value.keyword = keyword;
handleSearch(); handleSearch();
} else { } else {
searchForm.value.keyword = ""; searchForm.value.keyword = resourceStore.keyword;
} }
} }
); );
watch(
() => resourceStore.keyword,
(newKeyword) => {
searchForm.value.keyword = newKeyword;
}
);
// 方法定义 // 方法定义
const handleSearch = async () => { const handleSearch = async () => {
@@ -164,8 +170,6 @@ const handleLogout = () => {
padding-bottom: 100px; // tabbar高度 + 底部安全区域 padding-bottom: 100px; // tabbar高度 + 底部安全区域
box-sizing: border-box; box-sizing: border-box;
flex: 1; flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
} }
// 加载状态 // 加载状态

View File

@@ -121,7 +121,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, computed } from "vue"; import { ref, watch, onMounted, onUnmounted, computed, onBeforeUnmount } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { showToast } from "vant"; import { showToast } from "vant";
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
@@ -223,14 +223,14 @@ const searchMovieforTag = (tag: string) => {
// 使用节流包装加载更多函数 // 使用节流包装加载更多函数
const throttledLoadMore = throttle((channelId: string) => { const throttledLoadMore = throttle((channelId: string) => {
resourceStore.searchResources("", true, channelId); resourceStore.searchResources("", true, channelId);
}, 200); }, 2000);
// 滚动加载 // 滚动加载
const doScroll = () => { const doScroll = () => {
const appElement = document.querySelector("#app") as HTMLElement; const appElement = document.querySelector("#app") as HTMLElement;
if (appElement) { if (appElement) {
const { scrollHeight, scrollTop, clientHeight } = appElement; const { scrollHeight, scrollTop, clientHeight } = appElement;
if (scrollHeight - (clientHeight + scrollTop) <= 200) { if (scrollHeight - (clientHeight + scrollTop) <= 1) {
throttledLoadMore(currentTab.value); throttledLoadMore(currentTab.value);
} }
} }
@@ -258,6 +258,18 @@ watch(currentTab, () => {
appElement.scrollTo(0, 0); appElement.scrollTo(0, 0);
} }
}); });
// 页面进入 设置缓存的数据源
onMounted(() => {
const lastResourceList = localStorage.getItem("last_resource_list");
if (lastResourceList) {
resourceStore.resources = JSON.parse(lastResourceList).list;
}
});
// 页面销毁 清除搜索词
onBeforeUnmount(() => {
resourceStore.keyword = "";
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

6
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cloud-saver", "name": "cloud-saver",
"version": "0.2.0", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cloud-saver", "name": "cloud-saver",
"version": "0.2.0", "version": "0.2.2",
"workspaces": [ "workspaces": [
"frontend", "frontend",
"backend" "backend"
@@ -60,7 +60,7 @@
}, },
"frontend": { "frontend": {
"name": "cloud-saver-web", "name": "cloud-saver-web",
"version": "0.2.0", "version": "0.2.3",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.7", "axios": "^1.6.7",

View File

@@ -1,6 +1,6 @@
{ {
"name": "cloud-saver", "name": "cloud-saver",
"version": "0.2.1", "version": "0.2.3",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"frontend", "frontend",
@@ -17,6 +17,9 @@
"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",
"version:patch": "npm version patch -w frontend && npm version patch",
"version:minor": "npm version minor -w frontend && npm version minor",
"version:major": "npm version major -w frontend && npm version major",
"format": "prettier --write \"**/*.{js,ts,vue,json,css,scss}\"", "format": "prettier --write \"**/*.{js,ts,vue,json,css,scss}\"",
"format:check": "prettier --check \"**/*.{js,ts,vue,json,css,scss}\"", "format:check": "prettier --check \"**/*.{js,ts,vue,json,css,scss}\"",
"format:all": "npm run format && npm run lint:fix", "format:all": "npm run format && npm run lint:fix",