refactor:pc views

This commit is contained in:
jiangrui
2025-03-05 18:20:54 +08:00
parent 7bcec7e3b4
commit 1f3a83b84d
25 changed files with 2949 additions and 1117 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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({

View File

@@ -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>

View 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>