mirror of
https://github.com/jiangrui1994/CloudSaver.git
synced 2026-01-10 07:08:46 +08:00
refactor:pc views
This commit is contained in:
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@@ -13,6 +13,7 @@ declare module 'vue' {
|
||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
@@ -25,9 +26,11 @@ declare module 'vue' {
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElLoadingIcon: typeof import('element-plus/es')['ElLoadingIcon']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
@@ -4,7 +4,7 @@ export const userApi = {
|
||||
login: (data: { username: string; password: string }) => {
|
||||
return request.post<{ token: string }>("/api/user/login", data);
|
||||
},
|
||||
register: (data: { username: string; password: string }) => {
|
||||
register: (data: { username: string; password: string; registerCode: string }) => {
|
||||
return request.post<{ token: string }>("/api/user/register", data);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
<template>
|
||||
<div class="aside-menu">
|
||||
<div class="logo">
|
||||
<img :src="logo" class="logo-img" />
|
||||
<div class="logo-text">Cloud Saver</div>
|
||||
<div class="pc-aside">
|
||||
<!-- Logo 区域 -->
|
||||
<div class="pc-aside__logo">
|
||||
<img :src="logo" alt="Cloud Saver Logo" class="logo__image" />
|
||||
<h1 class="logo__title">Cloud Saver</h1>
|
||||
</div>
|
||||
|
||||
<!-- 菜单区域 -->
|
||||
<el-menu
|
||||
:default-active="currentMenu?.index || '1'"
|
||||
:default-openeds="currentMenuOpen"
|
||||
class="el-menu-vertical"
|
||||
@open="handleOpen"
|
||||
@close="handleClose"
|
||||
class="pc-aside__menu"
|
||||
>
|
||||
<template v-for="menu in menuList">
|
||||
<el-sub-menu v-if="menu.children" :key="menu.index" :index="menu.index">
|
||||
<template v-for="menu in menuList" :key="menu.index">
|
||||
<!-- 子菜单 -->
|
||||
<el-sub-menu v-if="menu.children" :index="menu.index">
|
||||
<template #title>
|
||||
<el-icon><component :is="menu.icon" /></el-icon>
|
||||
<span>{{ menu.title }}</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.index"
|
||||
:index="child.index"
|
||||
@click="handleMenuClick(child)"
|
||||
>
|
||||
{{ child.title }}
|
||||
<span>{{ child.title }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 普通菜单项 -->
|
||||
<el-menu-item
|
||||
v-else
|
||||
:key="menu.router"
|
||||
:index="menu.index"
|
||||
:disabled="menu.disabled"
|
||||
@click="handleMenuClick(menu)"
|
||||
@@ -38,54 +42,86 @@
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
|
||||
<!-- GitHub 链接 -->
|
||||
<div class="pc-aside__footer">
|
||||
<a
|
||||
href="https://github.com/jiangrui1994/CloudSaver"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="github-link"
|
||||
>
|
||||
<svg
|
||||
height="20"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
width="20"
|
||||
class="github-icon"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.5.75C6.146.75 1 5.896 1 12.25c0 5.089 3.292 9.387 7.863 10.91.575.101.79-.244.79-.546 0-.273-.014-1.178-.014-2.142-2.889.532-3.636-.704-3.866-1.35-.13-.331-.69-1.352-1.18-1.625-.402-.216-.977-.748-.014-.762.906-.014 1.553.834 1.769 1.179 1.035 1.74 2.688 1.25 3.349.948.1-.747.402-1.25.733-1.538-2.559-.287-5.232-1.279-5.232-5.678 0-1.25.445-2.285 1.178-3.09-.115-.288-.517-1.467.115-3.048 0 0 .963-.302 3.163 1.179.92-.259 1.897-.388 2.875-.388.977 0 1.955.13 2.875.388 2.2-1.495 3.162-1.179 3.162-1.179.633 1.581.23 2.76.115 3.048.733.805 1.179 1.825 1.179 3.09 0 4.413-2.688 5.39-5.247 5.678.417.36.776 1.05.776 2.128 0 1.538-.014 2.774-.014 3.162 0 .302.216.662.79.547C20.709 21.637 24 17.324 24 12.25 24 5.896 18.854.75 12.5.75Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>GitHub</span>
|
||||
<span class="version">v{{ pkg.version }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Search, Film, Setting } from "@element-plus/icons-vue";
|
||||
import logo from "@/assets/images/logo.png";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { Search, Film, Setting, Link } from "@element-plus/icons-vue";
|
||||
import logo from "@/assets/images/logo.png";
|
||||
import pkg from "../../package.json";
|
||||
|
||||
// 类型定义
|
||||
interface MenuItem {
|
||||
index: string;
|
||||
title: string;
|
||||
icon?: typeof Search | typeof Film | typeof Setting;
|
||||
icon?: typeof Search | typeof Film | typeof Setting | typeof Link;
|
||||
router?: string;
|
||||
children?: MenuItem[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// 路由相关
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 菜单配置
|
||||
const menuList: MenuItem[] = [
|
||||
{
|
||||
index: "2",
|
||||
index: "1",
|
||||
title: "资源搜索",
|
||||
icon: Search,
|
||||
router: "/",
|
||||
router: "/resource",
|
||||
},
|
||||
{
|
||||
index: "1",
|
||||
index: "2",
|
||||
title: "豆瓣榜单",
|
||||
icon: Film,
|
||||
children: [
|
||||
{
|
||||
index: "1-1",
|
||||
index: "2-1",
|
||||
title: "热门电影",
|
||||
router: "/douban?type=movie",
|
||||
},
|
||||
{
|
||||
index: "1-2",
|
||||
index: "2-2",
|
||||
title: "热门电视剧",
|
||||
router: "/douban?type=tv",
|
||||
},
|
||||
{
|
||||
index: "1-3",
|
||||
index: "2-3",
|
||||
title: "最新电影",
|
||||
router: "/douban?type=movie&tag=最新",
|
||||
},
|
||||
{
|
||||
index: "1-4",
|
||||
index: "2-4",
|
||||
title: "热门综艺",
|
||||
router: "/douban?type=tv&tag=综艺",
|
||||
},
|
||||
@@ -100,50 +136,171 @@ const menuList: MenuItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// 计算当前激活的菜单
|
||||
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 flatMenus = menuList.reduce<MenuItem[]>((acc, menu) => {
|
||||
if (!menu.children) {
|
||||
acc.push(menu);
|
||||
} else {
|
||||
acc.push(...menu.children);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return flatMenus.find((menu) => menu.router === decodeURIComponent(route.fullPath));
|
||||
});
|
||||
|
||||
// 计算当前展开的子菜单
|
||||
const currentMenuOpen = computed(() => {
|
||||
if (currentMenu.value && currentMenu.value.index.length > 1) {
|
||||
if (currentMenu.value?.index.includes("-")) {
|
||||
return [currentMenu.value.index.split("-")[0]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const handleOpen = (_key: string, _keyPath: string[]): void => {};
|
||||
const handleClose = (_key: string, _keyPath: string[]): void => {};
|
||||
const handleMenuClick = (menu: MenuItem): void => {
|
||||
|
||||
// 菜单点击处理
|
||||
const handleMenuClick = (menu: MenuItem) => {
|
||||
if (menu.router) {
|
||||
router.push(menu.router);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-menu-vertical {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
.logo-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 15px;
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.pc-aside {
|
||||
height: 100%;
|
||||
background: var(--theme-card-bg);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
// Logo 区域
|
||||
&__logo {
|
||||
@include flex-center;
|
||||
padding: 24px 16px;
|
||||
gap: 12px;
|
||||
|
||||
.logo__image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-primary);
|
||||
@include text-overflow;
|
||||
}
|
||||
}
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
|
||||
// 菜单区域
|
||||
&__menu {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
color: var(--theme-text-regular);
|
||||
|
||||
&.is-active {
|
||||
color: var(--theme-primary);
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu) {
|
||||
.el-sub-menu__title {
|
||||
color: var(--theme-text-regular);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// GitHub 链接区域
|
||||
&__footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
background: var(--theme-card-bg);
|
||||
|
||||
.github-link {
|
||||
@include flex-center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
color: var(--theme-text-regular);
|
||||
text-decoration: none;
|
||||
border-radius: var(--theme-radius);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
.github-icon {
|
||||
font-size: 20px;
|
||||
transition: var(--theme-transition);
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
transform: translateY(-1px);
|
||||
|
||||
.github-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.version {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滚动条
|
||||
.pc-aside__menu {
|
||||
height: calc(100vh - 80px - 69px); // 减去 logo 高度和 footer 高度
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
<template>
|
||||
<div class="folder-select">
|
||||
<div class="folder-select-header">
|
||||
当前位置:<el-icon style="margin: 0 5px"><Folder /></el-icon
|
||||
>{{ selectedFolder?.path?.map((x: Folder) => x.name).join("/") }}
|
||||
<div class="folder-header">
|
||||
<div class="folder-path">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<template v-if="folderPath.length">
|
||||
<span
|
||||
v-for="(folder, index) in folderPath"
|
||||
:key="folder.cid"
|
||||
class="path-item"
|
||||
@click="handlePathClick(index)"
|
||||
>
|
||||
<span class="folder-name">{{ folder.name }}</span>
|
||||
<el-icon v-if="index < folderPath.length - 1"><ArrowRight /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="root-path" @click="handlePathClick(-1)">根目录</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="folders"
|
||||
:props="defaultProps"
|
||||
node-key="cid"
|
||||
:load="loadNode"
|
||||
lazy
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<span class="folder-node">
|
||||
|
||||
<div class="folder-list">
|
||||
<div v-if="!folders.length" class="empty-folder">
|
||||
<el-empty description="暂无文件夹" />
|
||||
</div>
|
||||
<div
|
||||
v-for="folder in folders"
|
||||
:key="folder.cid"
|
||||
class="folder-item"
|
||||
:class="{ 'is-selected': folder.cid === selectedFolder?.cid }"
|
||||
@click="handleFolderClick(folder)"
|
||||
>
|
||||
<div class="folder-info">
|
||||
<el-icon><Folder /></el-icon>
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<span class="folder-name">{{ folder.name }}</span>
|
||||
</div>
|
||||
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -28,12 +48,9 @@
|
||||
import { ref, defineProps } from "vue";
|
||||
import { cloud115Api } from "@/api/cloud115";
|
||||
import { quarkApi } from "@/api/quark";
|
||||
import type { TreeInstance } from "element-plus";
|
||||
import type { Folder } from "@/types";
|
||||
import { type RequestResult } from "@/types/response";
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import type { Folder as FolderType } from "@/types";
|
||||
import { Folder, FolderOpened, ArrowRight, Loading } from "@element-plus/icons-vue";
|
||||
|
||||
const resourceStore = useResourceStore();
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -43,96 +60,218 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const treeRef = ref<TreeInstance>();
|
||||
const folders = ref<Folder[]>([]);
|
||||
const selectedFolder = ref<Folder | null>(null);
|
||||
const loading = ref(false);
|
||||
const folders = ref<FolderType[]>([]);
|
||||
const selectedFolder = ref<FolderType | null>(null);
|
||||
const folderPath = ref<FolderType[]>([{ name: "根目录", cid: "0" }]);
|
||||
const emit = defineEmits<{
|
||||
(e: "select", folderId: string): void;
|
||||
(e: "close"): void;
|
||||
}>();
|
||||
|
||||
const defaultProps = {
|
||||
label: "name",
|
||||
children: "children",
|
||||
isLeaf: "leaf",
|
||||
};
|
||||
|
||||
const cloudTypeApiMap = {
|
||||
pan115: cloud115Api,
|
||||
quark: quarkApi,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const loadNode = async (node: any, resolve: (list: Folder[]) => void) => {
|
||||
const getList = async (cid: string = "0") => {
|
||||
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
|
||||
loading.value = true;
|
||||
try {
|
||||
let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" };
|
||||
resourceStore.setLoadTree(true);
|
||||
if (node.level === 0) {
|
||||
if (api.getFolderList) {
|
||||
// 使用类型保护检查方法是否存在
|
||||
res = await api.getFolderList();
|
||||
}
|
||||
} else {
|
||||
if (api.getFolderList) {
|
||||
// 使用类型保护检查方法是否存在
|
||||
res = await api.getFolderList(node.data.cid);
|
||||
}
|
||||
}
|
||||
const res = await api.getFolderList?.(cid);
|
||||
if (res?.code === 0) {
|
||||
resolve(res.data.length ? res.data : []);
|
||||
folders.value = res.data || [];
|
||||
} else {
|
||||
throw new Error(res.message);
|
||||
throw new Error(res?.message);
|
||||
}
|
||||
resourceStore.setLoadTree(false);
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");
|
||||
// 关闭模态框
|
||||
ElMessage.error(error instanceof Error ? error.message : "获取目录失败");
|
||||
emit("close");
|
||||
resourceStore.setLoadTree(false);
|
||||
resolve([]);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeClick = (data: Folder) => {
|
||||
selectedFolder.value = {
|
||||
...data,
|
||||
path: data.path ? [...data.path, data] : [data],
|
||||
};
|
||||
emit("select", data.cid);
|
||||
const handleFolderClick = async (folder: FolderType) => {
|
||||
selectedFolder.value = folder;
|
||||
folderPath.value = [...folderPath.value, folder];
|
||||
emit("select", folder.cid);
|
||||
await getList(folder.cid);
|
||||
};
|
||||
|
||||
const handlePathClick = async (index: number) => {
|
||||
if (index < 0) {
|
||||
// 点击根目录
|
||||
folderPath.value = [{ name: "根目录", cid: "0" }];
|
||||
selectedFolder.value = null;
|
||||
await getList("0");
|
||||
} else {
|
||||
// 点击路径中的某个文件夹
|
||||
const targetFolder = folderPath.value[index];
|
||||
folderPath.value = folderPath.value.slice(0, index + 1);
|
||||
selectedFolder.value = targetFolder;
|
||||
await getList(targetFolder.cid);
|
||||
emit("select", targetFolder.cid);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
getList();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.folder-select {
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
|
||||
.folder-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: var(--theme-radius);
|
||||
|
||||
.folder-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--theme-text-regular);
|
||||
font-size: 14px;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
.path-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
|
||||
.folder-name {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.root-path {
|
||||
color: var(--theme-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
|
||||
.folder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--theme-radius);
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--theme-primary);
|
||||
|
||||
.el-icon {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.folder-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-regular);
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folder-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.empty-folder {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
@include flex-center;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
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;
|
||||
color: var(--theme-text-regular);
|
||||
|
||||
.loading-icon {
|
||||
font-size: 20px;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,61 +1,155 @@
|
||||
<template>
|
||||
<div class="resource-card-list">
|
||||
<div v-for="group in store.resources" :key="group.id" class="resource-list">
|
||||
<div class="group-header">
|
||||
<el-link :href="`https://t.me/s/${group.id}`" target="_blank" :underline="false">
|
||||
<el-image :src="group.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy />
|
||||
{{ group.channelInfo.name }}</el-link
|
||||
>
|
||||
<el-icon class="header-icon" @click="group.displayList = !group.displayList"
|
||||
><ArrowDown
|
||||
/></el-icon>
|
||||
</div>
|
||||
<div v-show="group.displayList" class="card-item-list">
|
||||
<div v-for="resource in group.list" :key="resource.messageId" class="card-item-content">
|
||||
<el-card class="card-item">
|
||||
<el-image
|
||||
class="card-item-image"
|
||||
:src="`/tele-images/?url=${encodeURIComponent(resource.image as string)}`"
|
||||
fit="cover"
|
||||
lazy
|
||||
:alt="resource.title"
|
||||
hide-on-click-modal
|
||||
:preview-src-list="[
|
||||
`${location.origin}/tele-images/?url=${encodeURIComponent(resource.image as string)}`,
|
||||
]"
|
||||
/>
|
||||
<el-link :href="resource.cloudLinks[0]" target="_blank" :underline="false"
|
||||
><div class="item-name">{{ resource.title }}</div></el-link
|
||||
>
|
||||
<div class="item-description" v-html="resource.content"></div>
|
||||
<div v-if="resource.tags && resource.tags.length" class="tags-list">
|
||||
<span>标签:</span>
|
||||
<div class="resource-card">
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showDetail"
|
||||
:title="currentResource?.title"
|
||||
width="700px"
|
||||
class="resource-detail-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="currentResource" class="detail-content">
|
||||
<div class="detail-cover">
|
||||
<el-image
|
||||
class="cover-image"
|
||||
:src="`/tele-images/?url=${encodeURIComponent(currentResource.image as string)}`"
|
||||
fit="cover"
|
||||
/>
|
||||
<el-tag
|
||||
class="cloud-type"
|
||||
:type="store.tagColor[currentResource.cloudType as keyof TagColor]"
|
||||
effect="dark"
|
||||
round
|
||||
>
|
||||
{{ currentResource.cloudType }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<h3 class="detail-title">
|
||||
<el-link :href="currentResource.cloudLinks[0]" target="_blank" :underline="false">
|
||||
{{ currentResource.title }}
|
||||
</el-link>
|
||||
</h3>
|
||||
<div class="detail-description" v-html="currentResource.content" />
|
||||
<div v-if="currentResource.tags?.length" class="detail-tags">
|
||||
<span class="tags-label">标签:</span>
|
||||
<div class="tags-list">
|
||||
<el-tag
|
||||
v-for="item in resource.tags"
|
||||
:key="item"
|
||||
class="resource_tag"
|
||||
@click="searchMovieforTag(item)"
|
||||
v-for="tag in currentResource.tags"
|
||||
:key="tag"
|
||||
class="tag-item"
|
||||
@click="searchMovieforTag(tag)"
|
||||
>
|
||||
{{ item }}
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="item-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="currentResource && handleSave(currentResource)"
|
||||
>转存</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div v-for="group in store.resources" :key="group.id" class="resource-group">
|
||||
<div class="group-header">
|
||||
<el-link
|
||||
class="group-title"
|
||||
:href="`https://t.me/s/${group.id}`"
|
||||
target="_blank"
|
||||
:underline="false"
|
||||
>
|
||||
<el-image :src="group.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy />
|
||||
<span>{{ group.channelInfo.name }}</span>
|
||||
<span class="item-count">({{ group.list.length }})</span>
|
||||
</el-link>
|
||||
|
||||
<el-tooltip effect="dark" :content="group.displayList ? '收起' : '展开'" placement="top">
|
||||
<el-button class="toggle-btn" type="text" @click="group.displayList = !group.displayList">
|
||||
<el-icon :class="{ 'is-active': group.displayList }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-show="group.displayList" class="group-content">
|
||||
<div class="card-grid">
|
||||
<el-card
|
||||
v-for="resource in group.list"
|
||||
:key="resource.messageId"
|
||||
class="resource-card-item"
|
||||
:body-style="{ padding: '0' }"
|
||||
>
|
||||
<div class="card-wrapper">
|
||||
<div class="card-cover">
|
||||
<el-image
|
||||
class="cover-image"
|
||||
:src="`/tele-images/?url=${encodeURIComponent(resource.image as string)}`"
|
||||
fit="cover"
|
||||
lazy
|
||||
:alt="resource.title"
|
||||
@click="showResourceDetail(resource)"
|
||||
/>
|
||||
<el-tag
|
||||
class="cloud-type"
|
||||
:type="store.tagColor[resource.cloudType as keyof TagColor]"
|
||||
effect="dark"
|
||||
round
|
||||
size="small"
|
||||
>
|
||||
{{ resource.cloudType }}
|
||||
</el-tag>
|
||||
<el-button @click="handleSave(resource)">转存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-body">
|
||||
<el-link
|
||||
class="card-title"
|
||||
:href="resource.cloudLinks[0]"
|
||||
target="_blank"
|
||||
:underline="false"
|
||||
>
|
||||
{{ resource.title }}
|
||||
</el-link>
|
||||
|
||||
<div
|
||||
class="card-content"
|
||||
@click="showResourceDetail(resource)"
|
||||
v-html="resource.content"
|
||||
/>
|
||||
|
||||
<div v-if="resource.tags?.length" class="card-tags">
|
||||
<span class="tags-label">标签:</span>
|
||||
<div class="tags-list">
|
||||
<el-tag
|
||||
v-for="tag in resource.tags"
|
||||
:key="tag"
|
||||
class="tag-item"
|
||||
@click="searchMovieforTag(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<el-button type="primary" @click="handleSave(resource)">转存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="group.displayList" class="load-more">
|
||||
<el-button @click="handleLoadMore(group.id)"> 加载更多 </el-button>
|
||||
|
||||
<div class="load-more">
|
||||
<el-button :loading="group.loading" @click="handleLoadMore(group.id)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
加载更多
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,19 +157,28 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import { computed } from "vue";
|
||||
import { ref } from "vue";
|
||||
import type { ResourceItem, TagColor } from "@/types";
|
||||
import { ArrowDown, Plus } from "@element-plus/icons-vue";
|
||||
|
||||
const store = useResourceStore();
|
||||
|
||||
const location = computed(() => window.location);
|
||||
const showDetail = ref(false);
|
||||
const currentResource = ref<ResourceItem | null>(null);
|
||||
|
||||
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
|
||||
|
||||
const handleSave = (resource: ResourceItem) => {
|
||||
if (showDetail.value) {
|
||||
showDetail.value = false;
|
||||
}
|
||||
emit("save", resource);
|
||||
};
|
||||
|
||||
const showResourceDetail = (resource: ResourceItem) => {
|
||||
currentResource.value = resource;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const searchMovieforTag = (tag: string) => {
|
||||
emit("searchMovieforTag", tag);
|
||||
};
|
||||
@@ -85,105 +188,391 @@ const handleLoadMore = (channelId: string) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.resource-list {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
background-color: var(--theme-other_background);
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.card-item-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 220px);
|
||||
grid-row-gap: 30px;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
/* 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;
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.resource-card {
|
||||
position: relative;
|
||||
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;
|
||||
|
||||
// 资源组
|
||||
.resource-group {
|
||||
background: var(--theme-card-bg);
|
||||
backdrop-filter: var(--theme-blur);
|
||||
-webkit-backdrop-filter: var(--theme-blur);
|
||||
margin-bottom: 24px;
|
||||
border-radius: var(--theme-radius);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
}
|
||||
.header-icon {
|
||||
cursor: pointer;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
||||
// 组标题
|
||||
.group-header {
|
||||
@include flex-center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
.group-title {
|
||||
@include flex-center;
|
||||
gap: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-primary);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
.channel-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--theme-text-regular);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组内容
|
||||
.group-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 卡片网格
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 24px;
|
||||
grid-auto-rows: min-content;
|
||||
}
|
||||
|
||||
// 资源卡片
|
||||
.resource-card-item {
|
||||
border-radius: var(--theme-radius);
|
||||
transition: var(--theme-transition);
|
||||
overflow: hidden;
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--theme-shadow);
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-cover {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 180px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--theme-radius);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-type {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.card-title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: var(--theme-text-primary);
|
||||
word-break: break-word;
|
||||
height: 3em;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text-regular);
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
margin-top: auto;
|
||||
max-height: 88px;
|
||||
overflow: hidden;
|
||||
|
||||
.tags-label {
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
margin-right: 8px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-height: 72px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-item {
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
margin: 0;
|
||||
height: 24px;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
border-color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
@include flex-center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
|
||||
.el-button {
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
min-width: 80px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
.load-more {
|
||||
@include flex-center;
|
||||
position: relative;
|
||||
padding: 32px 0 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--el-border-color-lighter) 20%,
|
||||
var(--el-border-color-lighter) 80%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.el-button {
|
||||
min-width: 160px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--theme-text-regular);
|
||||
background: var(--theme-card-bg);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
transition: var(--theme-transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
border-color: var(--theme-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
|
||||
&::after {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
color: var(--theme-text-secondary);
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 详情弹窗样式
|
||||
.resource-detail-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.detail-cover {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border-radius: var(--theme-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cloud-type {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.detail-title {
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px;
|
||||
line-height: 1.5;
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
|
||||
.detail-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text-regular);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
.tags-label {
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
|
||||
.tag-item {
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
border-color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,74 +1,178 @@
|
||||
<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 class="resource-select">
|
||||
<div class="select-header">
|
||||
<div class="select-info">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>已选择 {{ selectedCount }} 个文件</span>
|
||||
<span v-if="totalSize" class="total-size">({{ formattedFileSize(totalSize) }})</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="text" @click="handleSelectAll(!hasSelectedAll)">
|
||||
{{ hasSelectedAll ? "取消全选" : "全选" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-list">
|
||||
<div
|
||||
v-for="file in resourceStore.shareInfo.list"
|
||||
:key="file.fileId"
|
||||
class="file-item"
|
||||
:class="{ 'is-checked': isChecked(file.fileId) }"
|
||||
@click="toggleSelect(file)"
|
||||
>
|
||||
<el-checkbox :model-value="isChecked(file.fileId)" @click.stop>
|
||||
<div class="file-info">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span class="file-name">{{ file.fileName }}</span>
|
||||
<span v-if="file.fileSize" class="file-size">
|
||||
{{ formattedFileSize(file.fileSize) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import { formattedFileSize } from "@/utils/index";
|
||||
import { computed } from "vue";
|
||||
import type { ShareInfo } from "@/types";
|
||||
import { Document } from "@element-plus/icons-vue";
|
||||
|
||||
const resourceStore = useResourceStore();
|
||||
|
||||
const defaultProps = {
|
||||
isLeaf: "leaf",
|
||||
const selectedCount = computed(
|
||||
() => resourceStore.resourceSelect.filter((x) => x.isChecked).length
|
||||
);
|
||||
|
||||
const totalSize = computed(() =>
|
||||
resourceStore.resourceSelect
|
||||
.filter((x) => x.isChecked)
|
||||
.reduce((sum, item) => sum + (item.fileSize || 0), 0)
|
||||
);
|
||||
|
||||
const totalFiles = computed(() => resourceStore.shareInfo.list.length);
|
||||
|
||||
const hasSelectedAll = computed(() => selectedCount.value === totalFiles.value);
|
||||
|
||||
const isChecked = (fileId: string) => {
|
||||
return resourceStore.resourceSelect.find((x) => x.fileId === fileId)?.isChecked;
|
||||
};
|
||||
|
||||
const handleCheckChange = (data: ShareInfo) => {
|
||||
const toggleSelect = (file: ShareInfo) => {
|
||||
let resourceSelect = [...resourceStore.resourceSelect];
|
||||
resourceSelect.forEach((x) => {
|
||||
if (x.fileId === data.fileId) x.isChecked = !x.isChecked;
|
||||
});
|
||||
const item = resourceSelect.find((x) => x.fileId === file.fileId);
|
||||
if (item) {
|
||||
item.isChecked = !item.isChecked;
|
||||
resourceStore.setSelectedResource(resourceSelect);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
const resourceSelect = resourceStore.shareInfo.list.map((file) => ({
|
||||
fileId: file.fileId,
|
||||
fileName: file.fileName,
|
||||
fileSize: file.fileSize,
|
||||
isChecked: checked,
|
||||
}));
|
||||
resourceStore.setSelectedResource(resourceSelect);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.folder-select {
|
||||
min-height: 300px;
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/responsive.scss";
|
||||
|
||||
.resource-select {
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.folder-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.folder-path {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.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;
|
||||
.select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: var(--theme-radius);
|
||||
|
||||
.select-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--theme-text-regular);
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.total-size {
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
|
||||
.file-item {
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--theme-radius);
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--theme-text-primary);
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-regular);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
@include text-ellipsis;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--theme-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,14 +61,6 @@
|
||||
<span v-else>无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column label="地址">
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.cloudLinks[0]" target="_blank">
|
||||
{{ row.cloudLinks[0] }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="云盘类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="store.tagColor[row.cloudType as keyof TagColor]" effect="dark" round>
|
||||
@@ -195,4 +187,10 @@ const searchMovieforTag = (tag: string) => {
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.resource-table {
|
||||
position: relative;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,130 +1,199 @@
|
||||
<template>
|
||||
<div class="search-bar">
|
||||
<div class="search-bar__input">
|
||||
<input
|
||||
<div class="pc-search">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="pc-search__input">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
placeholder="请输入搜索关键词或输入链接直接解析"
|
||||
class="input-with-select"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-icon class="search_icon" size="28px" @click="handleSearch"><Search /></el-icon class="search_icon">
|
||||
</div>
|
||||
<div class="logout" alt="退出登录" @click="logout">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="退出登录"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon><SwitchButton class="logout-icon" /></el-icon>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<el-icon v-if="keyword" class="search-icon" @click="handleSearch">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 用户操作区 -->
|
||||
<div class="pc-search__actions">
|
||||
<el-tooltip effect="dark" content="退出登录" placement="bottom">
|
||||
<el-button class="logout-btn" type="text" @click="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref,watch } from "vue";
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import { useRoute,useRouter } from "vue-router";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const resourcStore = useResourceStore();
|
||||
const keyword = ref("");
|
||||
const routeKeyword = computed(() => route.query.keyword as string);
|
||||
const logout = () => {
|
||||
localStorage.removeItem("token");
|
||||
router.push({ path: "/login" });
|
||||
};
|
||||
const handleSearch = async () => {
|
||||
// 如果搜索内容是一个https的链接,则尝试解析链接
|
||||
if (keyword.value.startsWith("http")) {
|
||||
resourcStore.parsingCloudLink(keyword.value);
|
||||
return;
|
||||
}
|
||||
if (!keyword.value.trim()) {
|
||||
return;
|
||||
}
|
||||
await resourcStore.searchResources(keyword.value);
|
||||
if(route.path !== '/'){
|
||||
router.push({ path: '/' });
|
||||
}
|
||||
};
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Search, ArrowRight, SwitchButton } from "@element-plus/icons-vue";
|
||||
import { STORAGE_KEYS } from "@/constants/storage";
|
||||
|
||||
watch(() => routeKeyword.value, () => {
|
||||
if(routeKeyword.value){
|
||||
keyword.value = routeKeyword.value;
|
||||
// 路由相关
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const resourcStore = useResourceStore();
|
||||
|
||||
// 响应式数据
|
||||
const keyword = ref("");
|
||||
const routeKeyword = computed(() => route.query.keyword as string);
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
||||
router.push("/login");
|
||||
ElMessage.success("已退出登录");
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = async () => {
|
||||
const searchText = keyword.value.trim();
|
||||
if (!searchText) {
|
||||
ElMessage.warning("请输入搜索内容");
|
||||
return;
|
||||
}
|
||||
|
||||
// 链接解析处理
|
||||
if (searchText.startsWith("http")) {
|
||||
await resourcStore.parsingCloudLink(searchText);
|
||||
return;
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
await resourcStore.searchResources(searchText);
|
||||
if (route.path !== "/resource") {
|
||||
router.push("/resource");
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由参数变化
|
||||
watch(
|
||||
() => routeKeyword.value,
|
||||
(newKeyword) => {
|
||||
if (newKeyword) {
|
||||
keyword.value = newKeyword;
|
||||
handleSearch();
|
||||
} else {
|
||||
keyword.value = ''
|
||||
keyword.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.logout{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 28px;
|
||||
margin-left: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-bar__input {
|
||||
width: 100%;
|
||||
margin: 15px auto;
|
||||
position: relative;
|
||||
background-color: var(--theme-other_background);
|
||||
box-shadow: 0 4px 10px rgba(225, 225, 225, 0.3);
|
||||
height: 40px;
|
||||
border-radius: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.input-with-select {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
padding-left: 24px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 40px;
|
||||
line-height: 100%;
|
||||
border: unset;
|
||||
font-size: 18px;
|
||||
}
|
||||
.search_icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-bar_tips{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 20px auto;
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.pc-search {
|
||||
@include flex-center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
// 搜索输入区域
|
||||
&__input {
|
||||
flex: 1;
|
||||
min-width: 0; // 防止溢出
|
||||
|
||||
:deep(.el-input) {
|
||||
--el-input-height: 44px;
|
||||
|
||||
.el-input__wrapper {
|
||||
@include glass-effect;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--theme-radius);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
0 2px 4px rgba(0, 0, 0, 0.05),
|
||||
0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: var(--theme-transition);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-primary);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--theme-primary),
|
||||
0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: var(--theme-primary);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--theme-primary),
|
||||
0 4px 8px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 15px;
|
||||
color: var(--theme-text-primary);
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__prefix-inner {
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
color: var(--theme-text-secondary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--theme-primary);
|
||||
transition: var(--theme-transition);
|
||||
margin-left: 8px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-new {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.switch-source {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.switch-source .label {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.display-style {
|
||||
margin-left: 20px;
|
||||
// 操作区域
|
||||
&__actions {
|
||||
.logout-btn {
|
||||
@include glass-effect;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border-radius: var(--theme-radius);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
color: var(--theme-text-regular);
|
||||
transition: var(--theme-transition);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
|
||||
.el-icon {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
5
frontend/src/constants/storage.ts
Normal file
5
frontend/src/constants/storage.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const STORAGE_KEYS = {
|
||||
USERNAME: "saved_username",
|
||||
PASSWORD: "saved_password",
|
||||
TOKEN: "token",
|
||||
} as const;
|
||||
@@ -9,6 +9,7 @@ import { Lazyload } from "vant";
|
||||
import "vant/es/notify/style";
|
||||
import "vant/es/dialog/style";
|
||||
import "@/styles/responsive.scss";
|
||||
import "@/styles/common.scss";
|
||||
|
||||
import router from "./router/index";
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: () => import("@/views/Login.vue"),
|
||||
component: () => import("@/views/pc/Login.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { UserSettingStore } from "@/types/user";
|
||||
import type {
|
||||
UserSettingStore,
|
||||
GlobalSettingAttributes,
|
||||
UserSettingAttributes,
|
||||
} from "@/types/user";
|
||||
import { settingApi } from "@/api/setting";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
@@ -21,19 +25,20 @@ export const useUserSettingStore = defineStore("user", {
|
||||
this.userSettings = data.userSettings;
|
||||
}
|
||||
},
|
||||
async saveSettings() {
|
||||
if (this.userSettings) {
|
||||
const setting: UserSettingStore = {
|
||||
userSettings: this.userSettings,
|
||||
};
|
||||
if (this.globalSetting) setting.globalSetting = this.globalSetting;
|
||||
const res = await settingApi.saveSetting(setting);
|
||||
if (res) {
|
||||
this.getSettings();
|
||||
ElMessage.success("保存成功");
|
||||
}
|
||||
|
||||
async saveSettings(settings: {
|
||||
globalSetting?: GlobalSettingAttributes | null;
|
||||
userSettings: UserSettingAttributes;
|
||||
}) {
|
||||
try {
|
||||
await settingApi.saveSetting(settings);
|
||||
await this.getSettings();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setDisplayStyle(style: "table" | "card") {
|
||||
this.displayStyle = style;
|
||||
ElMessage.success(`切换成功,当前为${style}模式`);
|
||||
|
||||
62
frontend/src/styles/common.scss
Normal file
62
frontend/src/styles/common.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
// 颜色系统
|
||||
:root {
|
||||
// 主题色
|
||||
--theme-primary: #0066cc;
|
||||
--theme-primary-hover: #0256ac;
|
||||
--theme-success: #28cd41;
|
||||
--theme-warning: #ff9f0a;
|
||||
--theme-error: #ff3b30;
|
||||
|
||||
// 中性色
|
||||
--theme-bg: #f5f7fa;
|
||||
--theme-card-bg: rgba(255, 255, 255, 0.8);
|
||||
--theme-text-primary: #000000;
|
||||
--theme-text-regular: #424242;
|
||||
--theme-text-secondary: #6e6e6e;
|
||||
|
||||
// 特效
|
||||
--theme-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--theme-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
--theme-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
|
||||
// 圆角
|
||||
--theme-radius-sm: 8px;
|
||||
--theme-radius: 12px;
|
||||
--theme-radius-lg: 16px;
|
||||
|
||||
// 模糊效果
|
||||
--theme-blur: blur(12px);
|
||||
|
||||
// 动画
|
||||
--theme-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// Mixins
|
||||
@mixin glass-effect {
|
||||
background: var(--theme-card-bg);
|
||||
backdrop-filter: var(--theme-blur);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin text-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 通用动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export interface ResourceItem {
|
||||
export interface Resource {
|
||||
list: ResourceItem[];
|
||||
displayList?: boolean;
|
||||
loading?: boolean;
|
||||
channelInfo: {
|
||||
channelId: string;
|
||||
name: string;
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { GlobalSettingAttributes, UserSettingAttributes } from "@/types";
|
||||
export interface UserSettingStore {
|
||||
globalSetting?: GlobalSettingAttributes | null;
|
||||
userSettings: UserSettingAttributes;
|
||||
displayStyle?: "table" | "card";
|
||||
export interface GlobalSettingAttributes {
|
||||
httpProxyHost: string;
|
||||
httpProxyPort: string | number;
|
||||
isProxyEnabled: boolean;
|
||||
AdminUserCode: number;
|
||||
CommonUserCode: number;
|
||||
}
|
||||
|
||||
export interface UserSettingAttributes {
|
||||
cloud115Cookie: string;
|
||||
quarkCookie: string;
|
||||
}
|
||||
|
||||
export interface UserSettingStore {
|
||||
globalSetting: GlobalSettingAttributes | null;
|
||||
userSettings: UserSettingAttributes;
|
||||
displayStyle: "table" | "card";
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ElMessage } from "element-plus";
|
||||
import { isMobileDevice } from "@/utils/index";
|
||||
import { showNotify } from "vant";
|
||||
import { RequestResult } from "../types/response";
|
||||
import { STORAGE_KEYS } from "@/constants/storage";
|
||||
|
||||
const errorMessage = (message: string) => {
|
||||
if (isMobileDevice()) {
|
||||
@@ -31,7 +32,7 @@ function isLoginAndRedirect(url: string) {
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("token");
|
||||
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} else if (!isLoginAndRedirect(config.url || "")) {
|
||||
@@ -53,7 +54,7 @@ axiosInstance.interceptors.response.use(
|
||||
(error) => {
|
||||
if (error.response.status === 401) {
|
||||
errorMessage("登录过期,请重新登录");
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
||||
window.location.href = "/login";
|
||||
return Promise.reject(new Error("登录过期,请重新登录"));
|
||||
}
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
<template>
|
||||
<div class="movie-wall">
|
||||
<div v-for="movie in doubanStore.hotList" :key="movie.id" class="movie-item">
|
||||
<div class="movie-poster">
|
||||
<el-image
|
||||
class="movie-poster-img"
|
||||
:src="movie.cover"
|
||||
fit="cover"
|
||||
lazy
|
||||
:alt="movie.title"
|
||||
hide-on-click-modal
|
||||
:preview-src-list="[movie.cover]"
|
||||
/>
|
||||
<div class="movie-rate">
|
||||
{{ movie.rate }}
|
||||
</div>
|
||||
<div class="movie-poster-hover" @click="searchMovie(movie.title)">
|
||||
<div class="movie-search">
|
||||
<el-icon class="search_icon" size="28px"><Search /></el-icon>
|
||||
<div class="douban-page">
|
||||
<div class="movie-wall">
|
||||
<div v-for="movie in doubanStore.hotList" :key="movie.id" class="movie-item">
|
||||
<div class="movie-poster">
|
||||
<el-image
|
||||
class="movie-poster-img"
|
||||
:src="movie.cover"
|
||||
fit="cover"
|
||||
lazy
|
||||
:alt="movie.title"
|
||||
hide-on-click-modal
|
||||
:preview-src-list="[movie.cover]"
|
||||
/>
|
||||
<div class="movie-rate">
|
||||
{{ movie.rate }}
|
||||
</div>
|
||||
<div class="movie-poster-hover" @click="searchMovie(movie.title)">
|
||||
<div class="movie-search">
|
||||
<el-icon class="search_icon" size="28px"><Search /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="movie-info">
|
||||
<el-link :href="movie.url" target="_blank" :underline="false" class="movie-title">{{
|
||||
movie.title
|
||||
}}</el-link>
|
||||
<div class="movie-info">
|
||||
<el-link :href="movie.url" target="_blank" :underline="false" class="movie-title">{{
|
||||
movie.title
|
||||
}}</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,89 +61,136 @@ const searchMovie = (title: string) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
@import "@/styles/responsive.scss";
|
||||
|
||||
.douban-page {
|
||||
height: calc(100vh - 180px);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.movie-wall {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 200px);
|
||||
grid-row-gap: 15px;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.movie-item {
|
||||
width: 200px; /* 设置固定宽度 */
|
||||
overflow: hidden; /* 确保内容不会超出卡片 */
|
||||
text-align: center;
|
||||
background-color: #f9f9f9; /* 可选:设置背景颜色 */
|
||||
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12); /* 可选:设置阴影效果 */
|
||||
border-radius: 15px; /* 设置图片圆角 */
|
||||
width: 200px;
|
||||
background: var(--theme-card-bg);
|
||||
border-radius: var(--theme-radius);
|
||||
box-shadow: var(--theme-shadow);
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 0px;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
padding-bottom: 0;
|
||||
padding: 12px;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--theme-shadow-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.movie-poster-img {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
object-fit: cover; /* 确保图片使用cover模式 */
|
||||
border-radius: 15px; /* 设置图片圆角 */
|
||||
object-fit: cover;
|
||||
border-radius: var(--theme-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.movie-info {
|
||||
/* margin-top: 8px; */
|
||||
padding: 12px 0 4px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
.movie-title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 10px 0;
|
||||
color: var(--theme-text-primary);
|
||||
transition: var(--theme-transition);
|
||||
@include text-ellipsis;
|
||||
max-width: 100%;
|
||||
line-height: 1.2;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.movie-poster {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 15px;
|
||||
border-radius: var(--theme-radius);
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.movie-poster-hover {
|
||||
opacity: 0; /* 默认情况下隐藏 */
|
||||
transition: opacity 0.3s ease; /* 添加过渡效果 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
/* height: 100%; */
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.movie-poster:hover .movie-poster-hover {
|
||||
opacity: 1; /* 鼠标移入时显示 */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.movie-rate {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background-color: rgba(88, 83, 250, 0.8);
|
||||
background: var(--theme-primary);
|
||||
color: white;
|
||||
padding: 0px 8px;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--theme-radius-sm);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.movie-search {
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--theme-radius);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,93 +1,196 @@
|
||||
<template>
|
||||
<div v-loading="resourcStore.loading" class="home" element-loading-background="rgba(0,0,0,0.6)">
|
||||
<el-container>
|
||||
<el-aside width="200px"><aside-menu /></el-aside>
|
||||
<el-container class="home-main">
|
||||
<el-header :class="{ 'home-header': true, 'search-bar-active': !store.scrollTop }">
|
||||
<div class="pc-home" :class="{ 'is-loading': resourcStore.loading }">
|
||||
<!-- 主布局容器 -->
|
||||
<el-container class="pc-home__container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside width="220px" class="pc-home__aside">
|
||||
<aside-menu />
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container class="pc-home__main">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<el-header class="pc-home__header" :class="{ 'is-scrolled': !store.scrollTop }">
|
||||
<search-bar />
|
||||
</el-header>
|
||||
<el-main class="home-main-main">
|
||||
<router-view />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-main class="pc-home__content">
|
||||
<div class="content-wrapper">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-main>
|
||||
<!-- <el-aside class="home-aside"></el-aside> -->
|
||||
</el-container>
|
||||
</el-container>
|
||||
<el-backtop :bottom="100">
|
||||
<div
|
||||
style="
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
color: #1989fa;
|
||||
"
|
||||
>
|
||||
UP
|
||||
</div>
|
||||
</el-backtop>
|
||||
|
||||
<!-- 全局加载 -->
|
||||
<div v-if="resourcStore.loading" class="pc-home__loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span class="loading-text">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <login v-else /> -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import { useStore } from "@/stores/index";
|
||||
import { useUserSettingStore } from "@/stores/userSetting";
|
||||
import { onUnmounted } from "vue";
|
||||
import { throttle } from "@/utils/index";
|
||||
import { Loading } from "@element-plus/icons-vue";
|
||||
import "element-plus/es/components/loading/style/css";
|
||||
import AsideMenu from "@/components/AsideMenu.vue";
|
||||
import SearchBar from "@/components/SearchBar.vue";
|
||||
|
||||
// 状态管理
|
||||
const resourcStore = useResourceStore();
|
||||
const store = useStore();
|
||||
const settingStore = useUserSettingStore();
|
||||
settingStore.getSettings();
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY;
|
||||
if (scrollTop > 50) {
|
||||
store.scrollTop && store.setScrollTop(false);
|
||||
} else {
|
||||
!store.scrollTop && store.setScrollTop(true);
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
// 初始化设置
|
||||
onMounted(() => {
|
||||
settingStore.getSettings();
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
// 滚动处理
|
||||
const handleScroll = throttle(() => {
|
||||
const scrollTop = window.scrollY;
|
||||
store.setScrollTop(scrollTop <= 50);
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home {
|
||||
// padding: 20px;
|
||||
min-width: 1000px;
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.pc-home {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-text-primary);
|
||||
|
||||
// 主容器
|
||||
&__container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 侧边栏
|
||||
&__aside {
|
||||
background: var(--theme-card-bg);
|
||||
backdrop-filter: var(--theme-blur);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--theme-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容区
|
||||
&__main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 顶部搜索栏
|
||||
&__header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
padding: 16px;
|
||||
background: var(--theme-card-bg);
|
||||
backdrop-filter: var(--theme-blur);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&.is-scrolled {
|
||||
padding: 12px;
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
&__content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
height: 0;
|
||||
|
||||
.content-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
&__loading {
|
||||
@include flex-center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
animation: fadeIn 0.3s ease;
|
||||
|
||||
.loading-text {
|
||||
color: var(--theme-text-primary);
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
font-size: 24px;
|
||||
color: var(--theme-primary);
|
||||
animation: rotating 2s linear infinite;
|
||||
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
}
|
||||
}
|
||||
.home-header {
|
||||
height: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
// padding: 0;
|
||||
background-color: rgba(231, 235, 239, 0.7) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-radius: 0 0 5px 5px;
|
||||
// background-color: var(--theme-other_background);
|
||||
// box-shadow: 0 4px 10px rgba(225, 225, 225, 0.3);
|
||||
// border-radius: 20px;
|
||||
|
||||
// 加载动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
.home-main {
|
||||
width: 1000px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
|
||||
// 路由过渡动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.home-main-main {
|
||||
padding: 10px 15px;
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.home-aside {
|
||||
width: 300px;
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
<template>
|
||||
<div class="login-register">
|
||||
<div class="login-bg"></div>
|
||||
<el-card class="card">
|
||||
<!-- 登录与注册切换 -->
|
||||
<el-tabs v-model="activeTab" class="tabs">
|
||||
<el-tab-pane label="登录" name="login"></el-tab-pane>
|
||||
<el-tab-pane label="注册" name="register"> </el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<el-form
|
||||
v-if="activeTab === 'login'"
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-form-item prop="username" class="form-item">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
name="username"
|
||||
autocomplete="on"
|
||||
class="form-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" class="form-item">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
class="form-input"
|
||||
show-password="true"
|
||||
autocomplete="on"
|
||||
name="password"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" class="form-submit" @click="loginFormRefValidate">
|
||||
登录
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<el-form
|
||||
v-if="activeTab === 'register'"
|
||||
ref="registerFormRef"
|
||||
:model="registerForm"
|
||||
:rules="registerRules"
|
||||
label-position="top"
|
||||
><el-form-item prop="username" class="form-item">
|
||||
<el-input v-model="registerForm.username" placeholder="用户名" class="form-input" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" class="form-item">
|
||||
<el-input
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
class="form-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" class="form-item">
|
||||
<el-input
|
||||
v-model="password2"
|
||||
type="password"
|
||||
placeholder="再次输入密码"
|
||||
class="form-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="registerCode" class="form-item">
|
||||
<el-input v-model="registerForm.registerCode" placeholder="注册码" class="form-input" />
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" class="form-submit" @click="handleRegister"> 注册 </el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup type="ts">
|
||||
import { ref } from "vue";
|
||||
import { userApi } from "@/api/user";
|
||||
import router from "@/router";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const activeTab = ref("login"); // 默认显示登录表单
|
||||
|
||||
const loginForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const registerForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
registerCode: "",
|
||||
});
|
||||
|
||||
const password2 = ref("");
|
||||
|
||||
const loginRules = {
|
||||
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const registerRules = {
|
||||
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||
registerCode: [{ required: true, message: "请输入注册码", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const loginFormRef = ref(null);
|
||||
const registerFormRef = ref(null);
|
||||
|
||||
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const res = await userApi.login(loginForm.value);
|
||||
if (res.code === 0) {
|
||||
const { token } = res.data;
|
||||
localStorage.setItem("token", token);
|
||||
// 路由跳转首页
|
||||
router.push("/");
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("登录失败", error);
|
||||
}
|
||||
};
|
||||
const loginFormRefValidate = () => {
|
||||
loginFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
handleLogin();
|
||||
} else {
|
||||
ElMessage.error("登录表单验证失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
registerFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if(password2.value !== registerForm.value.password){
|
||||
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("注册表单验证失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-register {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.login-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url("../assets/images/login-bg.jpg");
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
-webkit-filter: blur(3px);
|
||||
-moz-filter: blur(3px);
|
||||
-o-filter: blur(3px);
|
||||
-ms-filter: blur(3px);
|
||||
filter: blur(3px);
|
||||
z-index: 0;
|
||||
}
|
||||
.card {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 480px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0px 20px 60px rgba(123, 61, 224, 0.1);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.form-input {
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-submit {
|
||||
margin-bottom: 10px;
|
||||
background-color: #6366f1;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.google-login {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-agreement {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,62 +1,105 @@
|
||||
<template>
|
||||
<div class="resource-list">
|
||||
<div :class="{ 'resource-list__header': true }">
|
||||
<div class="header_left">
|
||||
<div class="refresh_btn" @click="refreshResources">
|
||||
<el-icon class="type_icon" size="20px"><Refresh /></el-icon>最新资源<span
|
||||
class="item-count"
|
||||
>(上次刷新时间:{{ resourceStore.lastUpdateTime }})</span
|
||||
>
|
||||
</div>
|
||||
<div class="pc-resources">
|
||||
<!-- 头部工具栏 -->
|
||||
<div class="pc-resources__header">
|
||||
<div class="header__left">
|
||||
<el-tooltip effect="dark" content="点击获取最新资源" placement="bottom">
|
||||
<el-button class="refresh-btn" type="text" @click="refreshResources">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<span>最新资源</span>
|
||||
<span class="update-time"> (上次刷新时间:{{ resourceStore.lastUpdateTime }}) </span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="header_right">
|
||||
<el-icon
|
||||
v-if="userStore.displayStyle === 'card'"
|
||||
class="type_icon"
|
||||
@click="setDisplayStyle('table')"
|
||||
><Menu
|
||||
/></el-icon>
|
||||
<el-icon v-else class="type_icon" @click="setDisplayStyle('card')"><Fold /></el-icon>
|
||||
|
||||
<div class="header__right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="userStore.displayStyle === 'card' ? '切换到列表视图' : '切换到卡片视图'"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
class="view-toggle"
|
||||
@click="setDisplayStyle(userStore.displayStyle === 'card' ? 'table' : 'card')"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="userStore.displayStyle === 'card' ? 'Menu' : 'Grid'" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<ResourceTable
|
||||
v-if="userStore.displayStyle === 'table'"
|
||||
@load-more="handleLoadMore"
|
||||
@search-moviefor-tag="searchMovieforTag"
|
||||
@save="handleSave"
|
||||
></ResourceTable>
|
||||
<ResourceCard
|
||||
v-else
|
||||
@load-more="handleLoadMore"
|
||||
@search-moviefor-tag="searchMovieforTag"
|
||||
@save="handleSave"
|
||||
></ResourceCard>
|
||||
<el-empty v-if="resourceStore.resources.length === 0" :image-size="200" />
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div ref="contentRef" class="pc-resources__content">
|
||||
<component
|
||||
:is="userStore.displayStyle === 'table' ? ResourceTable : ResourceCard"
|
||||
v-if="resourceStore.resources.length > 0"
|
||||
@load-more="handleLoadMore"
|
||||
@search-moviefor-tag="searchMovieforTag"
|
||||
@save="handleSave"
|
||||
/>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="resourceStore.resources.length === 0" class="pc-resources__empty">
|
||||
<el-empty :image-size="200">
|
||||
<template #description>
|
||||
<p class="empty-text">暂无资源</p>
|
||||
<el-tooltip effect="dark" content="点击获取最新资源" placement="top">
|
||||
<el-button type="primary" @click="refreshResources">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<span>刷新资源</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回顶部 -->
|
||||
<el-backtop :bottom="40" :right="40" target=".pc-resources__content">
|
||||
<div class="pc-resources__backtop">
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</div>
|
||||
</el-backtop>
|
||||
|
||||
<!-- 保存对话框 -->
|
||||
<el-dialog
|
||||
v-if="currentResource"
|
||||
v-model="saveDialogVisible"
|
||||
:title="saveDialogMap[saveDialogStep].title"
|
||||
width="580px"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header="{ titleId }">
|
||||
<div class="my-header">
|
||||
<div :id="titleId">
|
||||
<el-tag
|
||||
:type="resourceStore.tagColor[currentResource.cloudType as keyof TagColor]"
|
||||
effect="dark"
|
||||
round
|
||||
>
|
||||
{{ currentResource.cloudType }}
|
||||
</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 class="dialog-header">
|
||||
<h3 :id="titleId">
|
||||
<div class="title-main">
|
||||
<el-tag
|
||||
:type="resourceStore.tagColor[currentResource.cloudType as keyof TagColor]"
|
||||
effect="dark"
|
||||
round
|
||||
>
|
||||
{{ currentResource.cloudType }}
|
||||
</el-tag>
|
||||
{{ saveDialogMap[saveDialogStep].title }}
|
||||
</div>
|
||||
<div class="title-sub">
|
||||
<span class="resource-title" :title="currentResource.title">
|
||||
{{ currentResource.title }}
|
||||
</span>
|
||||
<span
|
||||
v-if="resourceStore.shareInfo.fileSize && saveDialogStep === 1"
|
||||
class="file-size"
|
||||
>
|
||||
({{ formattedFileSize(resourceStore.shareInfo.fileSize) }})
|
||||
</span>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="resourceStore.loadTree">
|
||||
<resource-select
|
||||
v-if="saveDialogVisible && saveDialogStep === 1 && resourceStore.resourceSelect.length"
|
||||
@@ -70,13 +113,23 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="saveDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmClick">{{
|
||||
saveDialogMap[saveDialogStep].buttonText
|
||||
}}</el-button>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="saveDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmClick">
|
||||
{{ saveDialogMap[saveDialogStep].buttonText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="resourceStore.loading" class="pc-resources__loading">
|
||||
<div class="loading-text">加载中...</div>
|
||||
<div class="is-loading">
|
||||
<el-icon><Loading /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -93,6 +146,7 @@ import type { ResourceItem, TagColor } from "@/types";
|
||||
import ResourceCard from "@/components/Home/ResourceCard.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ArrowUp } from "@element-plus/icons-vue";
|
||||
const router = useRouter();
|
||||
|
||||
const resourceStore = useResourceStore();
|
||||
@@ -113,7 +167,7 @@ const saveDialogMap = {
|
||||
},
|
||||
2: {
|
||||
title: "选择保存目录",
|
||||
buttonText: "保存",
|
||||
buttonText: "保存到此目录",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -133,7 +187,8 @@ const handleFolderSelect = async (folderId: string) => {
|
||||
|
||||
const handleConfirmClick = async () => {
|
||||
if (saveDialogStep.value === 1) {
|
||||
if (resourceStore.resourceSelect.length === 0) {
|
||||
const selectedFiles = resourceStore.resourceSelect.filter((x) => x.isChecked);
|
||||
if (selectedFiles.length === 0) {
|
||||
ElMessage.warning("请选择要保存的资源");
|
||||
return;
|
||||
}
|
||||
@@ -161,75 +216,302 @@ const searchMovieforTag = (tag: string) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.resource-list {
|
||||
/* margin-top: 20px; */
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
@import "@/styles/responsive.scss";
|
||||
|
||||
.pc-resources {
|
||||
// 整体容器
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// 头部工具栏
|
||||
&__header {
|
||||
@include glass-effect;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 48px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--theme-radius);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-primary);
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
|
||||
.header__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
|
||||
.refresh-btn {
|
||||
@include flex-center;
|
||||
gap: 8px;
|
||||
color: var(--theme-text-regular);
|
||||
transition: var(--theme-transition);
|
||||
white-space: nowrap;
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
.update-time {
|
||||
margin-left: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
|
||||
.view-toggle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
color: var(--theme-text-regular);
|
||||
border-radius: var(--theme-radius);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
&__content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 180px);
|
||||
overflow-y: auto;
|
||||
|
||||
// 资源列表组件样式覆盖
|
||||
:deep(.resource-table),
|
||||
:deep(.resource-card) {
|
||||
height: 100%;
|
||||
|
||||
// 自定义滚动条
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
&__loading {
|
||||
@include glass-effect;
|
||||
@include flex-center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
animation: fadeIn 0.3s ease;
|
||||
|
||||
.loading-text {
|
||||
color: var(--theme-text-primary);
|
||||
font-size: 14px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
font-size: 24px;
|
||||
color: var(--theme-primary);
|
||||
animation: rotating 2s linear infinite;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
&__empty {
|
||||
@include flex-center;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
|
||||
.empty-text {
|
||||
color: var(--theme-text-primary);
|
||||
font-size: 14px;
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
@include flex-center;
|
||||
gap: 8px;
|
||||
padding: 8px 20px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
transition: var(--theme-transition);
|
||||
background: var(--theme-primary);
|
||||
border-color: var(--theme-primary);
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回顶部按钮
|
||||
&__backtop {
|
||||
@include flex-center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--theme-primary);
|
||||
background: var(--theme-card-bg);
|
||||
border-radius: var(--theme-radius);
|
||||
box-shadow: var(--theme-shadow);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-primary);
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.resource-list__header {
|
||||
height: 48px;
|
||||
background-color: var(--theme-other_background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 15px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.header_right {
|
||||
cursor: pointer;
|
||||
}
|
||||
.type_icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
size: 48px;
|
||||
}
|
||||
.refresh_btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
.item-count {
|
||||
color: #909399;
|
||||
font-size: 0.9em;
|
||||
|
||||
// 对话框样式
|
||||
.dialog-header {
|
||||
h3 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-primary);
|
||||
|
||||
.title-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--theme-text-secondary);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
max-width: 300px;
|
||||
@include text-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
:deep(.el-dialog) {
|
||||
border-radius: var(--theme-radius);
|
||||
overflow: hidden;
|
||||
|
||||
.item-count {
|
||||
color: #909399;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.el-dialog__header {
|
||||
margin: 0;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-column) {
|
||||
.cell {
|
||||
padding: 0 !important;
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__expanded-cell) {
|
||||
padding: 20px !important;
|
||||
// 表格扩展列样式
|
||||
:deep(.el-table) {
|
||||
.el-table__expand-column {
|
||||
.cell {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__expanded-cell {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-table__expand-icon {
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon) {
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
}
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
// 加载动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,175 +1,458 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<el-card v-if="settingStore.globalSetting" class="setting-card">
|
||||
<h2>网络配置</h2>
|
||||
<div class="section">
|
||||
<div class="form-group">
|
||||
<label for="proxyDomain">代理ip:</label>
|
||||
<el-input
|
||||
id="proxyDomain"
|
||||
v-model="globalSetting.httpProxyHost"
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="127.0.0.1"
|
||||
/>
|
||||
<div class="settings-page">
|
||||
<!-- 项目配置卡片 -->
|
||||
<el-card v-if="settingStore.globalSetting" class="settings-card network-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Connection /></el-icon>
|
||||
<h2>项目配置</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="proxyPort">代理端口:</label>
|
||||
<el-input
|
||||
id="proxyPort"
|
||||
v-model="globalSetting.httpProxyPort"
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="7890"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="settings-section">
|
||||
<!-- 代理配置组 -->
|
||||
<div class="settings-group">
|
||||
<div class="group-header">
|
||||
<h3>代理设置</h3>
|
||||
<el-switch
|
||||
v-model="localGlobalSetting.isProxyEnabled"
|
||||
active-text="已启用"
|
||||
@change="handleProxyChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<label for="proxyDomain">代理服务器IP</label>
|
||||
<el-input
|
||||
id="proxyDomain"
|
||||
v-model="localGlobalSetting.httpProxyHost"
|
||||
placeholder="127.0.0.1"
|
||||
:disabled="!localGlobalSetting.isProxyEnabled"
|
||||
@input="handleProxyHostChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Monitor /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label for="proxyPort">代理端口</label>
|
||||
<el-input
|
||||
id="proxyPort"
|
||||
v-model="localGlobalSetting.httpProxyPort"
|
||||
placeholder="7890"
|
||||
:disabled="!localGlobalSetting.isProxyEnabled"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Position /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="AdminUserCode">管理员注册码:</label>
|
||||
<el-input-number
|
||||
id="AdminUserCode"
|
||||
v-model="globalSetting.AdminUserCode"
|
||||
class="form-input"
|
||||
type="text"
|
||||
:controls="false"
|
||||
:precision="0"
|
||||
placeholder="设置管理员注册码"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="CommonUserCode">普通用户注册码:</label>
|
||||
<el-input-number
|
||||
id="CommonUserCode"
|
||||
v-model="globalSetting.CommonUserCode"
|
||||
class="form-input"
|
||||
type="text"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
placeholder="设置普通用户注册码"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="form-group">
|
||||
<label for="isProxyEnabled">启用代理:</label>
|
||||
<el-switch v-model="globalSetting.isProxyEnabled" @change="saveSettings" />
|
||||
|
||||
<!-- 注册码配置组 -->
|
||||
<div class="settings-group">
|
||||
<h3>注册码设置</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<label for="AdminUserCode">管理员注册码</label>
|
||||
<el-input-number
|
||||
id="AdminUserCode"
|
||||
v-model="localGlobalSetting.AdminUserCode"
|
||||
:controls="false"
|
||||
:precision="0"
|
||||
placeholder="设置管理员注册码"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Key /></el-icon>
|
||||
</template>
|
||||
</el-input-number>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label for="CommonUserCode">普通用户注册码</label>
|
||||
<el-input-number
|
||||
id="CommonUserCode"
|
||||
v-model="localGlobalSetting.CommonUserCode"
|
||||
:controls="false"
|
||||
:precision="0"
|
||||
placeholder="设置普通用户注册码"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Key /></el-icon>
|
||||
</template>
|
||||
</el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="setting-card">
|
||||
<h2>用户配置</h2>
|
||||
<div class="section">
|
||||
<div class="form-group">
|
||||
<label for="cookie115">115网盘Cookie:</label>
|
||||
<el-input
|
||||
id="cookie115"
|
||||
v-model="settingStore.userSettings.cloud115Cookie"
|
||||
class="form-input"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<!-- 用户配置卡片 -->
|
||||
<el-card class="settings-card user-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><User /></el-icon>
|
||||
<h2>用户配置</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cookieQuark">夸克网盘Cookie:</label>
|
||||
<el-input
|
||||
id="cookieQuark"
|
||||
v-model="settingStore.userSettings.quarkCookie"
|
||||
class="form-input"
|
||||
type="text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-group">
|
||||
<h3>网盘授权</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-item full-width">
|
||||
<label for="cookie115">115网盘 Cookie</label>
|
||||
<el-input
|
||||
id="cookie115"
|
||||
v-model="localUserSettings.cloud115Cookie"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入115网盘Cookie"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-item full-width">
|
||||
<label for="cookieQuark">夸克网盘 Cookie</label>
|
||||
<el-input
|
||||
id="cookieQuark"
|
||||
v-model="localUserSettings.quarkCookie"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入夸克网盘Cookie"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-setting-tips">
|
||||
<h3>帮助</h3>
|
||||
<div>
|
||||
<el-link
|
||||
target="_blank"
|
||||
href="https://alist.nn.ci/zh/guide/drivers/115.html#cookie%E8%8E%B7%E5%8F%96%E6%96%B9%E5%BC%8F"
|
||||
>如何获取115网盘cookie?</el-link
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<el-link target="_blank" href="https://alist.nn.ci/zh/guide/drivers/quark.html#cookie"
|
||||
>如何获取夸克网盘cookie?</el-link
|
||||
>
|
||||
|
||||
<!-- 帮助链接 -->
|
||||
<div class="settings-help">
|
||||
<h3>帮助文档</h3>
|
||||
<div class="help-links">
|
||||
<el-link
|
||||
href="https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l?singleDoc=true"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
CloudSaver部署与使用常见问题
|
||||
</el-link>
|
||||
<el-link
|
||||
href="https://www.yuque.com/xiaoruihenbangde/ggogn3/cl2g0p9h3xrgfa5i"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
CloudSaver功能介绍
|
||||
</el-link>
|
||||
<el-link
|
||||
href="https://alist.nn.ci/zh/guide/drivers/115.html#cookie获取方式"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
如何获取115网盘Cookie?
|
||||
</el-link>
|
||||
<el-link
|
||||
href="https://alist.nn.ci/zh/guide/drivers/quark.html#cookie"
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
如何获取夸克网盘Cookie?
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-button @click="saveSettings">保存设置</el-button>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="settings-actions">
|
||||
<el-button type="primary" @click="handleSave"> 保存设置 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserSettingStore } from "@/stores/userSetting";
|
||||
import { computed } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { GlobalSettingAttributes, UserSettingAttributes } from "@/types/user";
|
||||
import {
|
||||
Connection,
|
||||
Monitor,
|
||||
Position,
|
||||
Key,
|
||||
User,
|
||||
Lock,
|
||||
QuestionFilled,
|
||||
} from "@element-plus/icons-vue";
|
||||
|
||||
const settingStore = useUserSettingStore();
|
||||
|
||||
const globalSetting = computed(
|
||||
() =>
|
||||
settingStore.globalSetting || {
|
||||
httpProxyHost: "127.0.1",
|
||||
httpProxyPort: "7890",
|
||||
isProxyEnabled: false,
|
||||
AdminUserCode: 230713,
|
||||
CommonUserCode: 9527,
|
||||
// 本地状态
|
||||
const localGlobalSetting = ref<GlobalSettingAttributes>({
|
||||
httpProxyHost: "127.0.0.1",
|
||||
httpProxyPort: "7890",
|
||||
isProxyEnabled: false,
|
||||
AdminUserCode: 230713,
|
||||
CommonUserCode: 9527,
|
||||
});
|
||||
|
||||
const localUserSettings = ref<UserSettingAttributes>({
|
||||
cloud115Cookie: "",
|
||||
quarkCookie: "",
|
||||
});
|
||||
|
||||
// 监听 store 变化,更新本地状态
|
||||
watch(
|
||||
() => settingStore.globalSetting,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
localGlobalSetting.value = { ...newVal };
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingStore.userSettings,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
localUserSettings.value = { ...newVal };
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 初始化获取设置
|
||||
settingStore.getSettings();
|
||||
|
||||
const saveSettings = () => {
|
||||
settingStore.saveSettings();
|
||||
// Add your save logic here
|
||||
// 处理代理开关变化并立即保存
|
||||
const handleProxyChange = async (val: boolean) => {
|
||||
try {
|
||||
localGlobalSetting.value.isProxyEnabled = val;
|
||||
await settingStore.saveSettings({
|
||||
globalSetting: localGlobalSetting.value,
|
||||
userSettings: localUserSettings.value,
|
||||
});
|
||||
ElMessage.success("设置保存成功");
|
||||
} catch (error) {
|
||||
// 保存失败时恢复开关状态
|
||||
ElMessage.error("设置保存失败");
|
||||
localGlobalSetting.value.isProxyEnabled = !val;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理代理地址,去除协议前缀
|
||||
const handleProxyHostChange = (val: string) => {
|
||||
// 移除 http:// 或 https:// 前缀
|
||||
const cleanHost = val.replace(/^(https?:\/\/)/i, "");
|
||||
// 更新状态
|
||||
localGlobalSetting.value.httpProxyHost = cleanHost;
|
||||
};
|
||||
|
||||
// 其他设置的保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await settingStore.saveSettings({
|
||||
globalSetting: localGlobalSetting.value,
|
||||
userSettings: localUserSettings.value,
|
||||
});
|
||||
ElMessage.success("设置保存成功");
|
||||
} catch (error) {
|
||||
console.error("保存设置失败:", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings {
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.settings-page {
|
||||
// max-width: 1000px;
|
||||
margin: 0;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
margin-bottom: 24px;
|
||||
border-radius: var(--theme-radius);
|
||||
transition: var(--theme-transition);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--theme-shadow);
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@include flex-center;
|
||||
gap: 12px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 20px;
|
||||
}
|
||||
.setting-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 15px;
|
||||
|
||||
.settings-group {
|
||||
margin-bottom: 32px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-regular);
|
||||
}
|
||||
|
||||
.group-header {
|
||||
@include flex-center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
width: 48%;
|
||||
}
|
||||
.form-input {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
::v-deep .el-input__inner {
|
||||
text-align: left;
|
||||
.form-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
|
||||
.el-input__wrapper {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--theme-primary);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow:
|
||||
0 0 0 1px var(--theme-primary),
|
||||
0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__prefix-inner {
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
.settings-help {
|
||||
padding-top: 24px;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
.help-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-link) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-button {
|
||||
min-width: 120px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -80,6 +80,7 @@ import { showNotify } from "vant";
|
||||
import type { FieldInstance } from "vant";
|
||||
import { userApi } from "@/api/user";
|
||||
import logo from "@/assets/images/logo.png";
|
||||
import { STORAGE_KEYS } from "@/constants/storage";
|
||||
|
||||
// 类型定义
|
||||
interface LoginForm {
|
||||
@@ -94,7 +95,7 @@ const formData = ref<LoginForm>({
|
||||
});
|
||||
const isLoading = ref(false);
|
||||
const passwordRef = ref<FieldInstance>();
|
||||
const rememberPassword = ref(!!localStorage.getItem("rememberedPassword"));
|
||||
const rememberPassword = ref(false);
|
||||
|
||||
// 工具函数
|
||||
const router = useRouter();
|
||||
@@ -106,13 +107,12 @@ const focusPassword = () => {
|
||||
|
||||
// 在组件加载时检查是否有保存的账号密码
|
||||
onMounted(() => {
|
||||
if (rememberPassword.value) {
|
||||
const savedUsername = localStorage.getItem("username");
|
||||
const savedPassword = localStorage.getItem("password");
|
||||
if (savedUsername && savedPassword) {
|
||||
formData.value.username = savedUsername;
|
||||
formData.value.password = savedPassword;
|
||||
}
|
||||
const savedUsername = localStorage.getItem(STORAGE_KEYS.USERNAME);
|
||||
const savedPassword = localStorage.getItem(STORAGE_KEYS.PASSWORD);
|
||||
if (savedUsername && savedPassword) {
|
||||
formData.value.username = savedUsername;
|
||||
formData.value.password = savedPassword;
|
||||
rememberPassword.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,16 +124,14 @@ const handleSubmit = async () => {
|
||||
if (res.code === 0) {
|
||||
// 处理记住密码
|
||||
if (rememberPassword.value) {
|
||||
localStorage.setItem("username", formData.value.username);
|
||||
localStorage.setItem("password", formData.value.password);
|
||||
localStorage.setItem("rememberedPassword", "true");
|
||||
localStorage.setItem(STORAGE_KEYS.USERNAME, formData.value.username);
|
||||
localStorage.setItem(STORAGE_KEYS.PASSWORD, formData.value.password);
|
||||
} else {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("password");
|
||||
localStorage.removeItem("rememberedPassword");
|
||||
localStorage.removeItem(STORAGE_KEYS.USERNAME);
|
||||
localStorage.removeItem(STORAGE_KEYS.PASSWORD);
|
||||
}
|
||||
|
||||
localStorage.setItem("token", res.data.token);
|
||||
localStorage.setItem(STORAGE_KEYS.TOKEN, res.data.token);
|
||||
await router.push("/");
|
||||
} else {
|
||||
showNotify({
|
||||
|
||||
@@ -5,23 +5,36 @@
|
||||
<div class="setting__title">项目配置</div>
|
||||
<div class="setting__card">
|
||||
<van-cell-group inset>
|
||||
<van-field v-model="globalSetting.httpProxyHost" label="代理IP" placeholder="127.0.0.1" />
|
||||
<van-field v-model="globalSetting.httpProxyPort" label="代理端口" placeholder="7890" />
|
||||
<van-field
|
||||
v-model.number="globalSetting.AdminUserCode"
|
||||
v-model="localGlobalSetting.httpProxyHost"
|
||||
label="代理服务器IP"
|
||||
placeholder="127.0.0.1"
|
||||
@update:model-value="handleProxyHostChange"
|
||||
/>
|
||||
<van-field
|
||||
v-model="localGlobalSetting.httpProxyPort"
|
||||
label="代理端口"
|
||||
placeholder="7890"
|
||||
/>
|
||||
<van-field
|
||||
v-model.number="localGlobalSetting.AdminUserCode"
|
||||
label="管理员码"
|
||||
type="digit"
|
||||
placeholder="设置管理员注册码"
|
||||
/>
|
||||
<van-field
|
||||
v-model.number="globalSetting.CommonUserCode"
|
||||
v-model.number="localGlobalSetting.CommonUserCode"
|
||||
label="用户注册码"
|
||||
type="digit"
|
||||
placeholder="设置普通用户注册码"
|
||||
/>
|
||||
<van-cell center title="启用代理">
|
||||
<template #right-icon>
|
||||
<van-switch v-model="globalSetting.isProxyEnabled" size="24px" />
|
||||
<van-switch
|
||||
v-model="localGlobalSetting.isProxyEnabled"
|
||||
size="24px"
|
||||
@change="handleProxyChange"
|
||||
/>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
@@ -34,7 +47,7 @@
|
||||
<div class="setting__card">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="settingStore.userSettings.cloud115Cookie"
|
||||
v-model="localUserSettings.cloud115Cookie"
|
||||
label="115网盘"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
@@ -42,7 +55,7 @@
|
||||
placeholder="请输入115网盘Cookie"
|
||||
/>
|
||||
<van-field
|
||||
v-model="settingStore.userSettings.quarkCookie"
|
||||
v-model="localUserSettings.quarkCookie"
|
||||
label="夸克网盘"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
@@ -56,6 +69,16 @@
|
||||
<div class="setting__help">
|
||||
<div class="help__title">帮助说明</div>
|
||||
<div class="help__links">
|
||||
<van-cell
|
||||
title="CloudSaver部署与使用常见问题"
|
||||
is-link
|
||||
url="https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l?singleDoc=true"
|
||||
/>
|
||||
<van-cell
|
||||
title="CloudSaver功能介绍"
|
||||
is-link
|
||||
url="https://www.yuque.com/xiaoruihenbangde/ggogn3/cl2g0p9h3xrgfa5i"
|
||||
/>
|
||||
<van-cell
|
||||
title="如何获取115网盘cookie?"
|
||||
is-link
|
||||
@@ -72,39 +95,93 @@
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="setting__submit">
|
||||
<van-button round block type="primary" @click="saveSettings"> 保存设置 </van-button>
|
||||
<van-button round block type="primary" @click="handleSave"> 保存设置 </van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserSettingStore } from "@/stores/userSetting";
|
||||
import { computed } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { showNotify } from "vant";
|
||||
import type { GlobalSettingAttributes, UserSettingAttributes } from "@/types/user";
|
||||
|
||||
const settingStore = useUserSettingStore();
|
||||
|
||||
const globalSetting = computed(
|
||||
() =>
|
||||
settingStore.globalSetting || {
|
||||
httpProxyHost: "127.0.1",
|
||||
httpProxyPort: "7890",
|
||||
isProxyEnabled: false,
|
||||
AdminUserCode: 230713,
|
||||
CommonUserCode: 9527,
|
||||
// 本地状态
|
||||
const localGlobalSetting = ref<GlobalSettingAttributes>({
|
||||
httpProxyHost: "127.0.0.1",
|
||||
httpProxyPort: "7890",
|
||||
isProxyEnabled: false,
|
||||
AdminUserCode: 230713,
|
||||
CommonUserCode: 9527,
|
||||
});
|
||||
|
||||
const localUserSettings = ref<UserSettingAttributes>({
|
||||
cloud115Cookie: "",
|
||||
quarkCookie: "",
|
||||
});
|
||||
|
||||
// 监听 store 变化
|
||||
watch(
|
||||
() => settingStore.globalSetting,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
localGlobalSetting.value = { ...newVal };
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingStore.userSettings,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
localUserSettings.value = { ...newVal };
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 初始化获取设置
|
||||
settingStore.getSettings();
|
||||
|
||||
const saveSettings = async () => {
|
||||
// 处理代理开关变化并立即保存
|
||||
const handleProxyChange = async (val: boolean) => {
|
||||
try {
|
||||
await settingStore.saveSettings();
|
||||
localGlobalSetting.value.isProxyEnabled = val;
|
||||
await settingStore.saveSettings({
|
||||
globalSetting: localGlobalSetting.value,
|
||||
userSettings: localUserSettings.value,
|
||||
});
|
||||
showNotify({ type: "success", message: "代理设置已更新" });
|
||||
} catch (error) {
|
||||
showNotify({ type: "danger", message: "代理设置更新失败" });
|
||||
// 保存失败时恢复开关状态
|
||||
localGlobalSetting.value.isProxyEnabled = !val;
|
||||
}
|
||||
};
|
||||
|
||||
// 其他设置的保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await settingStore.saveSettings({
|
||||
globalSetting: localGlobalSetting.value,
|
||||
userSettings: localUserSettings.value,
|
||||
});
|
||||
showNotify({ type: "success", message: "设置保存成功" });
|
||||
} catch (error) {
|
||||
showNotify({ type: "danger", message: "设置保存失败" });
|
||||
}
|
||||
};
|
||||
|
||||
// 处理代理地址,去除协议前缀
|
||||
const handleProxyHostChange = (val: string) => {
|
||||
// 移除 http:// 或 https:// 前缀
|
||||
const cleanHost = val.replace(/^(https?:\/\/)/i, "");
|
||||
// 更新状态
|
||||
localGlobalSetting.value.httpProxyHost = cleanHost;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
354
frontend/src/views/pc/Login.vue
Normal file
354
frontend/src/views/pc/Login.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-bg"></div>
|
||||
<div class="login-card">
|
||||
<div class="card-header">
|
||||
<img src="@/assets/images/logo.png" alt="Logo" class="logo" />
|
||||
<h1 class="title">欢迎回来</h1>
|
||||
<p class="subtitle">登录您的账户以继续</p>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="login-tabs">
|
||||
<el-tab-pane label="登录" name="login">
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
:prefix-icon="User"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:prefix-icon="Lock"
|
||||
show-password
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<el-checkbox v-model="rememberPassword">记住密码</el-checkbox>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" class="submit-btn" :loading="loading" @click="handleLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="注册" name="register">
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="registerForm.username" placeholder="用户名" :prefix-icon="User" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:prefix-icon="Lock"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
placeholder="确认密码"
|
||||
:prefix-icon="Lock"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="registerCode">
|
||||
<el-input
|
||||
v-model="registerForm.registerCode"
|
||||
placeholder="注册码"
|
||||
:prefix-icon="Key"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" class="submit-btn" :loading="loading" @click="handleRegister">
|
||||
注册
|
||||
</el-button>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { User, Lock, Key } from "@element-plus/icons-vue";
|
||||
import { userApi } from "@/api/user";
|
||||
import "@/styles/common.scss";
|
||||
import { STORAGE_KEYS } from "@/constants/storage";
|
||||
import type { FormItemRule } from "element-plus";
|
||||
|
||||
// 状态
|
||||
const activeTab = ref("login");
|
||||
const loading = ref(false);
|
||||
const rememberPassword = ref(false);
|
||||
|
||||
const loginForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const registerForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
registerCode: "",
|
||||
});
|
||||
|
||||
// 表单校验规则
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: "请输入用户名", trigger: "blur" },
|
||||
{ min: 3, max: 20, message: "长度在 3 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
{ min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
const registerRules = {
|
||||
...loginRules,
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请确认密码", trigger: "blur" },
|
||||
{
|
||||
validator: (_rule: FormItemRule, value: string, callback: (error?: Error) => void) => {
|
||||
if (value !== registerForm.value.password) {
|
||||
callback(new Error("两次输入密码不一致"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
registerCode: [{ required: true, message: "请输入注册码", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const loginFormRef = ref();
|
||||
const registerFormRef = ref();
|
||||
|
||||
// 记住密码相关
|
||||
onMounted(() => {
|
||||
const savedUsername = localStorage.getItem(STORAGE_KEYS.USERNAME);
|
||||
const savedPassword = localStorage.getItem(STORAGE_KEYS.PASSWORD);
|
||||
if (savedUsername && savedPassword) {
|
||||
loginForm.value.username = savedUsername;
|
||||
loginForm.value.password = savedPassword;
|
||||
rememberPassword.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return;
|
||||
|
||||
await loginFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await userApi.login(loginForm.value);
|
||||
if (res.code === 0) {
|
||||
// 记住密码
|
||||
if (rememberPassword.value) {
|
||||
localStorage.setItem(STORAGE_KEYS.USERNAME, loginForm.value.username);
|
||||
localStorage.setItem(STORAGE_KEYS.PASSWORD, loginForm.value.password);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEYS.USERNAME);
|
||||
localStorage.removeItem(STORAGE_KEYS.PASSWORD);
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEYS.TOKEN, res.data.token);
|
||||
ElMessage.success("登录成功");
|
||||
router.push("/");
|
||||
} else {
|
||||
ElMessage.error(res.message || "登录失败");
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
ElMessage.error(error instanceof Error ? error.message : "登录失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 注册处理
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return;
|
||||
|
||||
await registerFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await userApi.register({
|
||||
username: registerForm.value.username,
|
||||
password: registerForm.value.password,
|
||||
registerCode: registerForm.value.registerCode,
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("注册成功");
|
||||
// 自动填充登录表单
|
||||
loginForm.value.username = registerForm.value.username;
|
||||
loginForm.value.password = registerForm.value.password;
|
||||
activeTab.value = "login";
|
||||
// 自动登录
|
||||
handleLogin();
|
||||
} else {
|
||||
ElMessage.error(res.message || "注册失败");
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
ElMessage.error(error instanceof Error ? error.message : "注册失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.login-page {
|
||||
@include flex-center;
|
||||
min-height: 100vh;
|
||||
background: var(--theme-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: url("@/assets/images/login-bg.jpg");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(3px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
@include glass-effect;
|
||||
position: relative;
|
||||
width: 420px;
|
||||
padding: 32px;
|
||||
border-radius: var(--theme-radius);
|
||||
box-shadow: var(--theme-shadow);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__active-bar) {
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
color: var(--theme-text-secondary);
|
||||
font-size: 16px;
|
||||
|
||||
&.is-active {
|
||||
color: var(--theme-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
box-shadow: none;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: var(--theme-primary);
|
||||
box-shadow: 0 0 0 1px var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
font-size: 16px;
|
||||
border-radius: var(--theme-radius-sm);
|
||||
background: var(--theme-primary);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-primary-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user