mirror of
https://github.com/jiangrui1994/CloudSaver.git
synced 2026-01-12 08:08:46 +08:00
refactor:pc views
This commit is contained in:
@@ -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