refactor:优化移动端页面

This commit is contained in:
jiangrui
2025-03-05 12:29:47 +08:00
parent 680108f499
commit 604ba2eec6
12 changed files with 793 additions and 309 deletions

View File

@@ -57,7 +57,6 @@ declare module 'vue' {
VanImage: typeof import('vant/es')['Image'] VanImage: typeof import('vant/es')['Image']
VanLoading: typeof import('vant/es')['Loading'] VanLoading: typeof import('vant/es')['Loading']
VanOverlay: typeof import('vant/es')['Overlay'] VanOverlay: typeof import('vant/es')['Overlay']
VanPopover: typeof import('vant/es')['Popover']
VanPopup: typeof import('vant/es')['Popup'] VanPopup: typeof import('vant/es')['Popup']
VanSearch: typeof import('vant/es')['Search'] VanSearch: typeof import('vant/es')['Search']
VanSwitch: typeof import('vant/es')['Switch'] VanSwitch: typeof import('vant/es')['Switch']

View File

@@ -2,7 +2,7 @@ module.exports = {
plugins: { plugins: {
"postcss-pxtorem": { "postcss-pxtorem": {
rootValue({ file }) { rootValue({ file }) {
return file.indexOf("mobile") !== -1 || file.indexOf("vant") !== -1 ? 37.5 : 75; return file.indexOf("vant") !== -1 || file.indexOf("mobile") !== -1 ? 50 : 75;
}, },
propList: ["*"], propList: ["*"],
exclude: (file) => { exclude: (file) => {

View File

@@ -1,38 +1,63 @@
<template> <template>
<div class="folder-select"> <div class="folder-select">
<div class="folder-select-header"> <!-- 面包屑导航 -->
当前位置<el-icon style="margin: 0 5px"><Folder /></el-icon> <div class="folder-select__nav">
<van-cell :border="false" class="nav-cell">
<template #title>
<div class="nav-breadcrumb">
<van-icon name="wap-home-o" class="home-icon" @click="handleHomeClick" />
<template v-for="(path, index) in currentFolderPath" :key="path.cid">
<van-icon v-if="index !== 0" name="arrow" />
<span <span
v-for="(path, index) in currentFolderPath"
:key="path.cid"
class="path-item" class="path-item"
:class="{ 'is-active': index === currentFolderPath.length - 1 }"
@click="handleFolderClick(path, index)" @click="handleFolderClick(path, index)"
> >
{{ path.name }} {{ path.name }}
<span v-if="index !== currentFolderPath.length - 1" class="path-separator">></span>
</span> </span>
</template>
</div> </div>
<div class="folder-item-list"> </template>
<div v-for="item in folders" :key="item.cid" class="folder-item" @click="getList(item)"> </van-cell>
<span class="folder-node">
<el-icon><Folder /></el-icon>
{{ item.name }}
</span>
</div> </div>
<!-- 文件夹列表 -->
<div class="folder-select__list">
<div v-if="resourceStore.loadTree" class="folder-select__loading">
<van-loading type="spinner" vertical>加载中...</van-loading>
</div>
<van-empty v-if="!resourceStore.loadTree && !folders.length" description="暂无文件夹" />
<van-cell-group v-if="!resourceStore.loadTree && folders.length" :border="false">
<van-cell
v-for="folder in folders"
:key="folder.cid"
:border="false"
clickable
@click="getList(folder)"
>
<template #icon>
<van-icon name="folder-o" class="folder-icon" />
</template>
<template #title>
<span class="folder-name">{{ folder.name }}</span>
</template>
<template #right-icon>
<van-icon name="arrow" />
</template>
</van-cell>
</van-cell-group>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, watch } from "vue"; import { ref, defineProps, onBeforeUnmount } from "vue";
import { cloud115Api } from "@/api/cloud115"; import { cloud115Api } from "@/api/cloud115";
import { quarkApi } from "@/api/quark"; import { quarkApi } from "@/api/quark";
import type { Folder } from "@/types"; import type { Folder } from "@/types";
import { type RequestResult } from "@/types/response"; import { type RequestResult } from "@/types/response";
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
import { showNotify } from "vant";
const resourceStore = useResourceStore();
import { ElMessage } from "element-plus";
const props = defineProps({ const props = defineProps({
cloudType: { cloudType: {
@@ -41,10 +66,12 @@ const props = defineProps({
}, },
}); });
const resourceStore = useResourceStore();
const folders = ref<Folder[]>([]); const folders = ref<Folder[]>([]);
const currentFolderPath = ref<Folder[]>([]); const currentFolderPath = ref<Folder[]>([]);
const emit = defineEmits<{ const emit = defineEmits<{
(e: "select", currentFolderPath: Folder[]): void; (e: "select", currentFolderPath: Folder[] | null): void;
(e: "close"): void; (e: "close"): void;
}>(); }>();
@@ -53,98 +80,182 @@ const cloudTypeApiMap = {
quark: quarkApi, quark: quarkApi,
}; };
const handleFolderClick = (folder: Folder, index: number) => { // 返回根目录
const current = { ...folder }; const handleHomeClick = () => {
currentFolderPath.value = currentFolderPath.value.slice(0, index); currentFolderPath.value = [];
getList(current); getList();
}; };
watch( const handleFolderClick = (folder: Folder, index: number) => {
() => currentFolderPath.value, currentFolderPath.value = currentFolderPath.value.slice(0, index + 1);
() => { getList(folder);
emit("select", currentFolderPath.value); };
},
{ deep: true } // 添加深度监听
);
const getList = async (data?: Folder) => { const getList = async (data?: Folder) => {
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap]; const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
try { try {
let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" };
resourceStore.setLoadTree(true); resourceStore.setLoadTree(true);
if (api.getFolderList) { const res: RequestResult<Folder[]> = await api.getFolderList?.(data?.cid || "0");
// 使用类型保护检查方法是否存在
res = await api.getFolderList(data?.cid || "0");
}
if (res?.code === 0) { if (res?.code === 0) {
folders.value = res.data.length ? res.data : []; folders.value = res.data || [];
currentFolderPath.value.push( if (!data) {
data || { currentFolderPath.value = [
{
name: "根目录", name: "根目录",
cid: "0", cid: "0",
},
];
} else if (!currentFolderPath.value.find((p) => p.cid === data.cid)) {
currentFolderPath.value.push(data);
} }
); emit("select", currentFolderPath.value);
} else { } else {
throw new Error(res.message); throw new Error(res.message);
} }
resourceStore.setLoadTree(false);
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败"); showNotify({
// 关闭模态框 type: "danger",
emit("close"); message: error instanceof Error ? error.message : "获取目录失败",
resourceStore.setLoadTree(false); });
currentFolderPath.value = [];
folders.value = []; folders.value = [];
emit("select", null);
emit("close");
} finally {
resourceStore.setLoadTree(false);
} }
}; };
// 初始化加载
getList(); getList();
// 组件销毁前重置状态
onBeforeUnmount(() => {
currentFolderPath.value = [];
folders.value = [];
emit("select", null);
});
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "@/styles/responsive.scss";
.folder-select { .folder-select {
position: relative; position: relative;
padding-top: var(--spacing-xl); height: 100%;
background: var(--theme-other_background);
display: flex;
flex-direction: column;
&-header { &__nav {
flex-shrink: 0;
border-bottom: 0.5px solid #f5f5f5;
background: var(--theme-other_background);
.nav-cell {
padding: 12px 16px;
min-height: 24px;
}
.nav-breadcrumb {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
font-size: var(--font-size-base); gap: 4px;
padding: var(--spacing-sm) var(--spacing-base); font-size: 14px;
border: 1px solid #e5e6e8; line-height: 1.4;
border-radius: var(--border-radius-base); min-height: 20px;
}
.home-icon {
font-size: 16px;
color: var(--theme-theme);
margin-right: 4px;
}
.path-item {
color: #666;
padding: 2px 4px;
border-radius: 4px;
&.is-active {
color: var(--theme-theme);
font-weight: 500;
}
&:active {
background-color: #f5f5f5;
}
}
}
&__list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
position: relative;
min-height: 200px;
display: flex;
flex-direction: column;
.van-empty {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.van-cell-group {
flex: 1;
}
}
&__loading {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; right: 0;
box-sizing: border-box; bottom: 0;
}
}
.folder-item {
font-size: var(--font-size-lg);
display: flex; display: flex;
align-items: center; align-items: center;
padding: var(--spacing-base) var(--spacing-sm); justify-content: center;
border-bottom: 1px dashed #ececec; background: rgba(255, 255, 255, 0.9);
z-index: 0;
.folder-node { .van-loading {
display: flex; padding: 16px 24px;
align-items: center; background: rgba(0, 0, 0, 0.7);
gap: var(--spacing-sm); border-radius: 8px;
color: #fff;
}
} }
}
.path-item { .folder-icon {
cursor: pointer; font-size: 20px;
&:hover {
color: var(--theme-theme); color: var(--theme-theme);
margin-right: 8px;
}
.folder-name {
font-size: 15px;
color: var(--theme-color);
} }
} }
.path-separator { // 深度修改 Vant 组件样式
margin: 0 var(--spacing-xs); :deep(.van-cell) {
padding: 12px 16px;
&::after {
display: none;
}
&:active {
background-color: #f5f5f5;
}
}
:deep(.van-empty) {
padding: 32px 0;
margin: 0;
} }
</style> </style>

View File

@@ -7,7 +7,7 @@
<div class="content__image"> <div class="content__image">
<van-image <van-image
:src="`/tele-images/?url=${encodeURIComponent(item.image as string)}`" :src="`/tele-images/?url=${encodeURIComponent(item.image as string)}`"
fit="contain" fit="cover"
lazy-load lazy-load
/> />
<!-- 来源标签移到图片左上角 --> <!-- 来源标签移到图片左上角 -->
@@ -125,10 +125,10 @@ const toggleExpand = (id: string) => {
} }
.resource-card { .resource-card {
padding: var(--spacing-base); padding: 5px 10px;
&__item { &__item {
margin-bottom: var(--spacing-base); margin-bottom: 12px;
background: var(--theme-other_background); background: var(--theme-other_background);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
overflow: hidden; overflow: hidden;
@@ -138,8 +138,8 @@ const toggleExpand = (id: string) => {
.item { .item {
&__content { &__content {
display: flex; display: flex;
gap: var(--spacing-base); gap: 16px;
padding: var(--spacing-base); padding: 16px;
} }
} }
@@ -240,11 +240,21 @@ const toggleExpand = (id: string) => {
&__action { &__action {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding: 4px 0;
.van-button { .van-button {
font-size: 12px; font-size: 13px;
height: 24px; height: 32px;
padding: 0 12px; padding: 0 20px;
:deep(.van-button__text) {
font-weight: 500;
font-size: 14px;
}
&:active {
opacity: 0.8;
}
} }
} }
} }

View File

@@ -1,81 +1,184 @@
<template> <template>
<div class="resource-select"> <div class="resource-select">
<van-checkbox-group v-model="selectedResourceIds" @change="handleCheckChange"> <van-checkbox-group v-model="selectedResourceIds">
<div v-for="item in resourceStore.shareInfo.list" :key="item.fileId" class="resource-item"> <van-cell-group :border="false">
<div class="resource-item-left"> <van-cell
<span class="resource-node"> v-for="item in resourceStore.shareInfo.list"
<el-icon><Folder /></el-icon> :key="item.fileId"
<div class="resource-name"> class="resource-item"
{{ item.fileName }} :border="false"
<span v-if="item.fileSize" class="file-size"> center
({{ formattedFileSize(item.fileSize) }}) @click="handleItemClick(item.fileId)"
>
<template #title>
<div class="resource-item__content">
<van-icon name="folder-o" class="content__icon" />
<div class="content__info">
<span class="info__name">{{ item.fileName }}</span>
<span v-if="item.fileSize" class="info__size">
{{ formattedFileSize(item.fileSize) }}
</span> </span>
</div> </div>
</span>
</div>
<div class="resource-item-right">
<van-checkbox :name="item.fileId"></van-checkbox>
</div>
</div> </div>
</template>
<template #right-icon>
<van-checkbox
:name="item.fileId"
class="resource-item__checkbox"
@click.stop="handleItemClick(item.fileId)"
/>
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group> </van-checkbox-group>
<!-- 空状态 -->
<van-empty v-if="!resourceStore.shareInfo.list?.length" description="暂无可选资源" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref, watch } from "vue";
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
import { formattedFileSize } from "@/utils/index"; import { formattedFileSize } from "@/utils/index";
const resourceStore = useResourceStore(); const resourceStore = useResourceStore();
const selectedResourceIds = ref<string[]>(); const selectedResourceIds = ref<string[]>([]);
selectedResourceIds.value = resourceStore.resourceSelect.map((x) => x.fileId);
const handleCheckChange = (Ids: string[]) => { // 初始化选中状态
selectedResourceIds.value = resourceStore.resourceSelect
.filter((x) => x.isChecked)
.map((x) => x.fileId);
// 监听选中状态变化
watch(selectedResourceIds, (newIds) => {
const newResourceSelect = [...resourceStore.resourceSelect]; const newResourceSelect = [...resourceStore.resourceSelect];
newResourceSelect.forEach((x) => { newResourceSelect.forEach((x) => {
x.isChecked = Ids.includes(x.fileId); x.isChecked = newIds.includes(x.fileId);
}); });
resourceStore.setSelectedResource(newResourceSelect); resourceStore.setSelectedResource(newResourceSelect);
});
// 添加点击处理函数
const handleItemClick = (fileId: string) => {
const index = selectedResourceIds.value.indexOf(fileId);
if (index === -1) {
selectedResourceIds.value.push(fileId);
} else {
selectedResourceIds.value.splice(index, 1);
}
}; };
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "@/styles/responsive.scss"; // 工具类
@mixin text-ellipsis {
.resource-select { overflow: hidden;
min-height: 300px; text-overflow: ellipsis;
max-height: 500px; white-space: nowrap;
overflow-y: auto;
padding: var(--spacing-base);
} }
.resource-item { .resource-select {
display: flex; height: 100%;
align-items: center; background: var(--theme-other_background);
justify-content: space-between; width: 100%;
padding: var(--spacing-base) 0; overflow-x: hidden;
border-bottom: 1px dashed #ececec;
&-left { .resource-item {
position: relative;
&__content {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px 0;
margin-right: 40px;
.content__icon {
flex-shrink: 0;
font-size: 20px;
color: var(--theme-theme);
margin-top: 2px;
}
.content__info {
flex: 1; flex: 1;
margin-right: var(--spacing-base); min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
.info__name {
font-size: 15px;
line-height: 1.4;
color: var(--van-text-color);
word-break: break-all;
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.info__size {
font-size: 13px;
color: var(--van-gray-6);
@include text-ellipsis;
}
}
}
&__checkbox {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
:deep(.van-checkbox__icon) {
font-size: 18px;
cursor: pointer;
.van-icon {
border-radius: 2px;
transition: all 0.2s;
}
}
}
&:active {
background-color: var(--van-active-color);
}
} }
} }
.resource-node { // 深度修改 Vant 组件样式
display: flex; :deep(.van-cell) {
align-items: center; align-items: flex-start;
gap: var(--spacing-sm); padding: 0 16px;
font-size: var(--font-size-lg); width: 100%;
box-sizing: border-box;
min-height: 60px;
position: relative;
&::after {
display: none;
}
.van-cell__title {
flex: 1;
min-width: 0;
}
} }
.resource-name { :deep(.van-checkbox__icon--checked) {
word-break: break-all; .van-icon {
background-color: var(--theme-theme);
border-color: var(--theme-theme);
}
} }
.file-size { :deep(.van-empty) {
font-size: var(--font-size-sm); padding: 32px 0;
color: #999; background: transparent;
margin-left: var(--spacing-xs);
} }
</style> </style>

View File

@@ -5,6 +5,7 @@ import { ElMessage } from "element-plus";
interface StoreType { interface StoreType {
hotList: HotListItem[]; hotList: HotListItem[];
loading: boolean;
currentParams: CurrentParams; currentParams: CurrentParams;
} }
@@ -16,6 +17,7 @@ interface CurrentParams {
export const useDoubanStore = defineStore("douban", { export const useDoubanStore = defineStore("douban", {
state: (): StoreType => ({ state: (): StoreType => ({
hotList: [], hotList: [],
loading: false,
currentParams: { currentParams: {
type: "movie", type: "movie",
tag: "热门", tag: "热门",
@@ -24,6 +26,7 @@ export const useDoubanStore = defineStore("douban", {
actions: { actions: {
async getHotList() { async getHotList() {
this.loading = true;
try { try {
const params = { const params = {
type: this.currentParams.type, type: this.currentParams.type,
@@ -40,6 +43,8 @@ export const useDoubanStore = defineStore("douban", {
} }
} catch (error) { } catch (error) {
ElMessage.error(error || "获取热门列表失败"); ElMessage.error(error || "获取热门列表失败");
} finally {
this.loading = false;
} }
}, },
setCurrentParams(currentParams: CurrentParams) { setCurrentParams(currentParams: CurrentParams) {

View File

@@ -1,3 +1,9 @@
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 响应式布局工具类 // 响应式布局工具类
@mixin mobile { @mixin mobile {
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
@@ -19,25 +25,25 @@
// 通用样式变量 // 通用样式变量
:root { :root {
// 字体大小 // 字体大小 - 整体缩小约25%
--font-size-xs: 12px; --font-size-xs: 20px; // 原24px
--font-size-sm: 14px; --font-size-sm: 22px; // 原26px
--font-size-base: 16px; --font-size-base: 24px; // 原28px
--font-size-lg: 18px; --font-size-lg: 28px; // 原32px
--font-size-xl: 20px; --font-size-xl: 32px; // 原36px
// 间距 // 间距 - 也相应缩小
--spacing-xs: 4px; --spacing-xs: 6px; // 原8px
--spacing-sm: 8px; --spacing-sm: 10px; // 原12px
--spacing-base: 16px; --spacing-base: 14px; // 原16px
--spacing-lg: 24px; --spacing-lg: 20px; // 原24px
--spacing-xl: 32px; --spacing-xl: 28px; // 原32px
// 圆角 // 圆角 - 适当调整
--border-radius-sm: 4px; --border-radius-sm: 6px; // 原8px
--border-radius-base: 8px; --border-radius-base: 10px; // 原12px
--border-radius-lg: 16px; --border-radius-lg: 14px; // 原16px
--border-radius-xl: 24px; --border-radius-xl: 20px; // 原24px
// 移动端特殊变量 // 移动端特殊变量
@include mobile { @include mobile {
@@ -45,3 +51,27 @@
--spacing-base: 12px; --spacing-base: 12px;
} }
} }
// 移动端适配
@media screen and (max-width: 768px) {
:root {
// 间距
--spacing-xs: 3px;
--spacing-sm: 5px;
--spacing-base: 7px;
--spacing-lg: 10px;
--spacing-xl: 14px;
// 字体大小
--font-size-xs: 10px;
--font-size-sm: 11px;
--font-size-base: 12px;
--font-size-lg: 14px;
--font-size-xl: 16px;
// 圆角
--border-radius-sm: 3px;
--border-radius-base: 5px;
--border-radius-lg: 7px;
}
}

View File

@@ -14,3 +14,16 @@ export function isMobileDevice() {
window.innerWidth <= 768 window.innerWidth <= 768
); );
} }
export function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): T {
let lastTime = 0;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
} as T;
}

View File

@@ -1,7 +1,12 @@
<template> <template>
<div class="mobile-page douban"> <div class="mobile-page douban">
<!-- 加载状态 -->
<div v-if="doubanStore.loading" class="douban__loading">
<van-loading type="spinner" size="24px" vertical>加载中...</van-loading>
</div>
<!-- 电影列表 --> <!-- 电影列表 -->
<div class="douban__grid"> <div v-else class="douban__grid">
<div v-for="movie in doubanStore.hotList" :key="movie.id" class="douban__item"> <div v-for="movie in doubanStore.hotList" :key="movie.id" class="douban__item">
<!-- 海报卡片 --> <!-- 海报卡片 -->
<div class="douban__poster"> <div class="douban__poster">
@@ -37,7 +42,7 @@
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->
<van-empty v-if="!doubanStore.hotList.length" description="暂无数据" /> <van-empty v-if="!doubanStore.loading && !doubanStore.hotList.length" description="暂无数据" />
</div> </div>
</template> </template>
@@ -174,6 +179,24 @@ const getRateColor = (rate: string | number) => {
} }
} }
} }
// 加载状态
&__loading {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--theme-background);
z-index: 1;
:deep(.van-loading) {
padding: 16px 24px;
background: rgba(0, 0, 0, 0.7);
border-radius: 8px;
color: #fff;
}
}
} }
// 深度修改 Vant 组件样式 // 深度修改 Vant 组件样式

View File

@@ -31,7 +31,7 @@
<!-- 底部导航栏 --> <!-- 底部导航栏 -->
<van-tabbar class="home__tabbar" route> <van-tabbar class="home__tabbar" route>
<van-tabbar-item to="/resource" icon="search">搜索</van-tabbar-item> <van-tabbar-item to="/resource" icon="search">搜索</van-tabbar-item>
<van-tabbar-item to="/douban" icon="video">影视</van-tabbar-item> <van-tabbar-item to="/douban" icon="video">热门</van-tabbar-item>
<van-tabbar-item to="/setting" icon="setting-o">设置</van-tabbar-item> <van-tabbar-item to="/setting" icon="setting-o">设置</van-tabbar-item>
</van-tabbar> </van-tabbar>
@@ -118,6 +118,8 @@ const handleLogout = () => {
// 布局 // 布局
min-height: 100vh; min-height: 100vh;
background: var(--theme-background); background: var(--theme-background);
display: flex;
flex-direction: column;
// 头部搜索 // 头部搜索
&__header { &__header {
@@ -159,9 +161,11 @@ const handleLogout = () => {
// 主内容区 - 调整顶部间距 // 主内容区 - 调整顶部间距
&__content { &__content {
padding-top: 64px; // 搜索框高度(48px) + 上下padding(8px * 2) padding-top: 64px; // 搜索框高度(48px) + 上下padding(8px * 2)
padding-bottom: calc(50px + var(--safe-area-bottom)); // tabbar高度 + 底部安全区域 padding-bottom: 100px; // tabbar高度 + 底部安全区域
min-height: 100vh;
box-sizing: border-box; box-sizing: border-box;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
} }
// 加载状态 // 加载状态

View File

@@ -7,7 +7,7 @@
<main class="login__content"> <main class="login__content">
<!-- 头部 Logo --> <!-- 头部 Logo -->
<header class="login__header"> <header class="login__header">
<img :src="logo" alt="Cloud Saver Logo" class="login__logo" width="50" height="50" /> <img :src="logo" alt="Cloud Saver Logo" class="login__logo" width="60" height="60" />
<h1 class="login__title">Cloud Saver</h1> <h1 class="login__title">Cloud Saver</h1>
</header> </header>
@@ -156,7 +156,6 @@ const handleSubmit = async () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.login { .login {
// 布局
position: relative; position: relative;
height: 100vh; height: 100vh;
width: 100%; width: 100%;
@@ -167,7 +166,8 @@ const handleSubmit = async () => {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: url("@/assets/images/mobile-login-bg.png") no-repeat; background: url("@/assets/images/mobile-login-bg.png") no-repeat;
background-size: 100% auto; background-size: cover;
background-position: center;
} }
// 主内容区 // 主内容区
@@ -176,10 +176,10 @@ const handleSubmit = async () => {
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
min-height: 60%; min-height: 65%;
padding: var(--spacing-lg); padding: 40px 20px;
background-color: var(--theme-other_background); background-color: var(--theme-other_background);
border-radius: var(--border-radius-xl) var(--border-radius-xl) 0 0; border-radius: 24px 24px 0 0;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
@@ -189,87 +189,100 @@ const handleSubmit = async () => {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-bottom: var(--spacing-xl); margin-bottom: 40px;
} }
&__logo { &__logo {
width: 50px; width: 60px;
height: 50px; height: 60px;
margin-right: var(--spacing-base); margin-right: 12px;
object-fit: contain; object-fit: contain;
} }
&__title { &__title {
margin: 0; margin: 0;
font-size: var(--font-size-xl); font-size: 28px;
font-weight: 700; font-weight: 600;
color: var(--theme-theme); color: var(--theme-theme);
} }
// 表单 // 表单
&__form { &__form {
padding: 0 var(--spacing-base); padding: 0;
} }
&__form-group { &__form-group {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); margin: 0 12px;
border-radius: var(--border-radius-lg); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border-radius: 12px;
overflow: hidden; overflow: hidden;
} }
&__submit { &__submit {
margin-top: var(--spacing-xl); margin: 32px 12px 0;
} }
&__button { &__button {
height: 40px; height: 48px;
font-size: 14px; font-size: 16px;
font-weight: 500; font-weight: 500;
} }
// 新增记住密码容器样式 // 记住密码区域
&__remember { &__remember {
padding: var(--spacing-sm) var(--spacing-lg); padding: 12px 16px;
border-top: 1px solid #f5f5f5; border-top: 0.5px solid #f5f5f5;
} }
} }
// Vant 组件样式覆盖 // Vant 组件样式优化
:deep(.van-cell-group--inset) { :deep(.van-cell-group--inset) {
margin: 0; margin: 0;
} }
:deep(.van-field) { :deep(.van-field) {
padding: var(--spacing-base); padding: 16px;
}
:deep(.van-field__label) { .van-field__label {
width: 4em; width: 4em;
color: var(--theme-color); color: var(--theme-color);
font-size: 15px;
}
.van-field__value {
.van-field__body {
input {
font-size: 15px;
}
}
}
.van-field__left-icon {
margin-right: 8px;
font-size: 18px;
}
} }
:deep(.van-field__left-icon) { // 记住密码复选框样式优化
margin-right: var(--spacing-sm);
}
// 优化记住密码样式
:deep(.remember-checkbox) { :deep(.remember-checkbox) {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 13px; font-size: 14px;
color: #666; color: #666;
.van-checkbox__icon { .van-checkbox__icon {
font-size: 14px; font-size: 16px;
.van-icon { .van-icon {
border: 1px solid #dcdee0; border: 1px solid #dcdee0;
border-radius: 2px;
transition: all 0.2s; transition: all 0.2s;
} }
} }
.van-checkbox__label { .van-checkbox__label {
margin-left: 6px; margin-left: 6px;
line-height: 1;
} }
&.van-checkbox--checked { &.van-checkbox--checked {

View File

@@ -1,20 +1,31 @@
<template> <template>
<div class="resource-list"> <div ref="listRef" class="resource-list">
<!-- 头部刷新区 --> <!-- 头部刷新区 -->
<div class="resource-list__header"> <van-cell-group :border="false" class="resource-list__header">
<van-cell center @click="refreshResources"> <van-cell center clickable @click="refreshResources">
<template #icon>
<van-icon name="replay" class="header__icon" />
</template>
<template #title> <template #title>
<div class="header__title"> <div class="header__content">
<van-icon name="replay" size="18" /> <span class="content__title">最新资源</span>
<span class="title__text">最新资源</span> <span class="content__tip">(点击可获取最新资源)</span>
<span class="title__time">上次刷新{{ resourceStore.lastUpdateTime }}</span>
</div> </div>
</template> </template>
<template #label>
<span class="header__time">上次刷新{{ resourceStore.lastUpdateTime }}</span>
</template>
</van-cell> </van-cell>
</div> </van-cell-group>
<!-- 资源列表 --> <!-- 资源列表 -->
<van-tabs v-model:active="currentTab" sticky swipeable animated> <van-tabs
v-model:active="currentTab"
swipeable
animated
class="resource-list__tabs"
:border="false"
>
<van-tab <van-tab
v-for="item in resourceStore.resources" v-for="item in resourceStore.resources"
:key="item.id" :key="item.id"
@@ -36,21 +47,30 @@
closeable closeable
position="bottom" position="bottom"
:style="{ height: '80%' }" :style="{ height: '80%' }"
class="save-popup"
> >
<div class="save-popup__container">
<!-- 弹窗头部 --> <!-- 弹窗头部 -->
<div class="popup__header"> <div class="save-popup__header">
<van-tag :color="getTagColor(currentResource?.cloudType)" round> <van-tag :color="getTagColor(currentResource?.cloudType)" round size="medium">
{{ currentResource?.cloudType }} {{ currentResource?.cloudType }}
</van-tag> </van-tag>
<span class="header__title">{{ saveDialogMap[saveDialogStep].title }}</span> <span class="header__title">{{ saveDialogMap[saveDialogStep].title }}</span>
<span v-if="resourceStore.shareInfo.fileSize && saveDialogStep === 1" class="header__size"> <span
v-if="resourceStore.shareInfo.fileSize && saveDialogStep === 1"
class="header__size"
>
{{ formattedFileSize(resourceStore.shareInfo.fileSize) }} {{ formattedFileSize(resourceStore.shareInfo.fileSize) }}
</span> </span>
</div> </div>
<!-- 弹窗内容 --> <!-- 弹窗内容 -->
<div class="popup__content"> <div class="save-popup__content">
<van-loading v-if="resourceStore.loadTree" vertical>加载中...</van-loading> <van-empty v-if="resourceStore.loadTree && saveDialogStep === 1" class="content__loading">
<template #image>
<van-loading size="24px" vertical>加载中...</van-loading>
</template>
</van-empty>
<resource-select <resource-select
v-if="saveDialogVisible && saveDialogStep === 1 && resourceStore.resourceSelect.length" v-if="saveDialogVisible && saveDialogStep === 1 && resourceStore.resourceSelect.length"
@@ -66,18 +86,36 @@
</div> </div>
<!-- 弹窗底部 --> <!-- 弹窗底部 -->
<div class="popup__footer"> <div class="save-popup__footer">
<div class="footer__path"> <van-cell class="footer__path" :border="false">
<span class="path__label">保存至</span> <template #title>
<div class="path__label">保存至</div>
</template>
<template #value>
<div class="path__value"> <div class="path__value">
<van-icon name="notes-o" /> <van-icon name="folder-o" class="value__icon" />
<span>{{ getCurrentFolderName }}</span> <span
class="value__text"
:class="{ 'value__text--placeholder': !currentFolderPath }"
>
{{ getCurrentFolderName }}
</span>
</div> </div>
</div> </template>
<van-button round type="primary" block @click="handleConfirmClick"> </van-cell>
<van-button
round
block
type="primary"
size="large"
:loading="isLoading"
@click="handleConfirmClick"
>
{{ saveDialogMap[saveDialogStep].buttonText }} {{ saveDialogMap[saveDialogStep].buttonText }}
</van-button> </van-button>
</div> </div>
</div>
</van-popup> </van-popup>
</div> </div>
</template> </template>
@@ -87,7 +125,7 @@ import { ref, watch, onMounted, onUnmounted, computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { showToast } from "vant"; import { showToast } from "vant";
import { useResourceStore } from "@/stores/resource"; import { useResourceStore } from "@/stores/resource";
import { formattedFileSize } from "@/utils/index"; import { formattedFileSize, throttle } from "@/utils/index";
import type { Folder, ResourceItem } from "@/types"; import type { Folder, ResourceItem } from "@/types";
import FolderSelect from "@/components/mobile/FolderSelect.vue"; import FolderSelect from "@/components/mobile/FolderSelect.vue";
import ResourceSelect from "@/components/mobile/ResourceSelect.vue"; import ResourceSelect from "@/components/mobile/ResourceSelect.vue";
@@ -104,6 +142,8 @@ const currentFolderId = ref<string | null>(null);
const currentFolderPath = ref<Folder[] | null>(null); const currentFolderPath = ref<Folder[] | null>(null);
const saveDialogStep = ref<1 | 2>(1); const saveDialogStep = ref<1 | 2>(1);
const currentTab = ref<string>(""); const currentTab = ref<string>("");
const isLoading = ref(false);
const listRef = ref<HTMLElement | null>(null);
// 计算属性 // 计算属性
const getCurrentFolderName = computed(() => { const getCurrentFolderName = computed(() => {
@@ -152,10 +192,10 @@ const handleSave = async (resource: ResourceItem) => {
} }
}; };
const handleFolderSelect = (folders: Folder[]) => { const handleFolderSelect = (folders: Folder[] | null) => {
if (!currentResource.value) return; if (!currentResource.value) return;
currentFolderPath.value = folders; currentFolderPath.value = folders;
currentFolderId.value = folders[folders.length - 1]?.cid || "0"; currentFolderId.value = folders?.[folders.length - 1]?.cid || "0";
}; };
const handleConfirmClick = async () => { const handleConfirmClick = async () => {
@@ -176,62 +216,138 @@ const handleSaveBtnClick = async () => {
await resourceStore.saveResource(currentResource.value, currentFolderId.value); await resourceStore.saveResource(currentResource.value, currentFolderId.value);
}; };
const handleLoadMore = (channelId: string) => {
resourceStore.searchResources("", true, channelId);
};
const searchMovieforTag = (tag: string) => { const searchMovieforTag = (tag: string) => {
router.push({ path: "/resource", query: { keyword: tag } }); router.push({ path: "/resource", query: { keyword: tag } });
}; };
// 使用节流包装加载更多函数
const throttledLoadMore = throttle((channelId: string) => {
resourceStore.searchResources("", true, channelId);
}, 200);
// 滚动加载 // 滚动加载
const doScroll = () => { const doScroll = () => {
const { scrollHeight, scrollTop, clientHeight } = document.documentElement; const appElement = document.querySelector("#app") as HTMLElement;
if (clientHeight + scrollTop >= scrollHeight - 50) { if (appElement) {
handleLoadMore(currentTab.value); const { scrollHeight, scrollTop, clientHeight } = appElement;
if (scrollHeight - (clientHeight + scrollTop) <= 200) {
throttledLoadMore(currentTab.value);
}
} }
}; };
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
window.addEventListener("scroll", doScroll); const appElement = document.querySelector("#app");
if (appElement) {
appElement.addEventListener("scroll", doScroll);
}
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("scroll", doScroll); const appElement = document.querySelector("#app");
if (appElement) {
appElement.removeEventListener("scroll", doScroll);
}
});
// 监听标签页切换
watch(currentTab, () => {
const appElement = document.querySelector("#app");
if (appElement) {
appElement.scrollTo(0, 0);
}
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.resource-list { .resource-list {
&__header { min-height: 100%;
margin-bottom: var(--spacing-base); background: var(--van-background);
padding-bottom: 20px;
.header__title { &__header {
margin-bottom: 8px;
background: var(--theme-other_background);
:deep(.van-cell) {
padding: 12px 16px;
min-height: 24px;
}
.header__icon {
font-size: 30px;
color: var(--theme-theme);
margin-right: 10px;
line-height: 1;
}
.header__content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: 6px;
font-size: 14px;
.title__text { .content__title {
font-size: 15px;
font-weight: 500; font-weight: 500;
line-height: 1.4;
} }
.title__time { .content__tip {
color: var(--van-gray-6);
font-size: 12px; font-size: 12px;
color: var(--van-gray-6);
background: var(--van-gray-1);
padding: 2px 6px;
border-radius: 4px;
line-height: 1.4;
} }
} }
.header__time {
font-size: 12px;
color: var(--van-gray-6);
line-height: 1.4;
margin-top: 2px;
}
}
&__tabs {
:deep(.van-tabs__wrap) {
background: var(--theme-other_background);
}
:deep(.van-tab) {
font-size: 14px;
padding: 0 20px;
height: 44px;
line-height: 44px;
}
:deep(.van-tabs__line) {
background: var(--theme-theme);
}
:deep(.van-tabs__content) {
padding: 8px 0;
}
} }
} }
.popup { .save-popup {
&__container {
height: 100%;
display: flex;
flex-direction: column;
}
&__header { &__header {
padding: var(--spacing-lg); flex-shrink: 0;
border-bottom: 1px solid var(--van-gray-3); padding: 16px;
border-bottom: 0.5px solid var(--van-gray-3);
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: 8px;
padding-right: 40px;
.header__title { .header__title {
font-size: 16px; font-size: 16px;
@@ -239,57 +355,114 @@ onUnmounted(() => {
} }
.header__size { .header__size {
margin-left: auto;
font-size: 13px;
color: var(--van-gray-6); color: var(--van-gray-6);
font-size: 14px; max-width: 120px;
}
}
&__content {
height: calc(100% - 140px);
padding: var(--spacing-base);
overflow-y: auto;
.van-loading {
margin: var(--spacing-xl) auto;
}
}
&__footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: var(--spacing-base);
background: var(--theme-other_background);
border-top: 1px solid var(--van-gray-3);
.footer__path {
margin-bottom: var(--spacing-base);
.path__label {
font-size: 14px;
margin-right: var(--spacing-xs);
}
.path__value {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--van-gray-2);
border-radius: var(--border-radius-sm);
font-size: 14px;
max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }
.van-button { &__content {
height: 40px; flex: 1;
overflow-y: auto;
background: var(--van-background-2);
.content__loading {
padding: 32px 0;
}
}
&__footer {
flex-shrink: 0;
padding: 12px 16px 16px;
background: var(--theme-other_background);
border-top: 0.5px solid var(--van-gray-3);
.footer__path {
margin: 0 0 12px;
:deep(.van-cell__title) {
flex: none;
padding-right: 8px;
display: flex;
align-items: center;
}
:deep(.van-cell__value) {
flex: 1;
}
.path__label {
font-size: 14px; font-size: 14px;
color: var(--van-text-color);
font-weight: 500;
white-space: nowrap;
}
.path__value {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: var(--van-gray-1);
border-radius: 4px;
width: 100%;
box-sizing: border-box;
.value__icon {
font-size: 16px;
color: var(--theme-theme);
flex-shrink: 0;
}
.value__text {
font-size: 14px;
color: var(--van-text-color);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
display: block;
&--placeholder {
color: var(--van-gray-6);
}
}
}
}
:deep(.van-cell__value) {
flex: 1;
text-align: left;
}
.van-button {
height: 44px;
font-size: 16px;
font-weight: 500;
} }
} }
} }
// 全局样式优化
:deep(.van-cell) {
padding: 16px 20px;
&::after {
display: none;
}
}
:deep(.van-popup) {
max-height: 90vh;
}
:deep(.van-overlay) {
background-color: rgba(0, 0, 0, 0.7);
}
</style> </style>