完善资源详情页

This commit is contained in:
李志强 2025-12-26 11:34:10 +08:00
parent 5c21f20126
commit 46fc7b2739
5 changed files with 1069 additions and 1379 deletions

View File

@ -0,0 +1,445 @@
<template>
<div class="resources-page">
<div class="container">
<div class="top-content">
<h1 class="page-title">{{ config.title }}</h1>
<div class="content flex flex-column align-items-center">
<p>{{ config.description }}</p>
</div>
</div>
<div class="main-content">
<!-- 左侧分类导航 -->
<div class="left-menu">
<div class="menu-title">分类导航</div>
<ul class="category-list">
<li
v-for="category in categories"
:key="category.id"
class="category-item"
:class="{ active: selectedCategory === category.id }"
@click="selectCategory(category.id)"
>
{{ category.name }}
</li>
</ul>
</div>
<!-- 右侧内容区域 -->
<div class="contents">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="resources.length === 0" class="no-data">暂无内容</div>
<div v-else class="resources-grid">
<div
v-for="resource in resources"
:key="resource.id"
class="resource-card"
>
<div class="resource-image">
<img :src="getImageUrl(resource.icon)" :alt="resource.title" />
</div>
<div class="card-header">
<router-link
:to="`/resource?id=${resource.id}&path=${config.routePath}&category=${encodeURIComponent(currentCategoryName)}`"
class="resource-title-link"
>
<h3 class="resource-title">{{ resource.title }}</h3>
</router-link>
<div class="resource-meta">
<span class="resource-view">
<i class="fa-solid fa-eye"></i>
{{ resource.view || 0 }}
</span>
<span class="resource-likes">
<i class="fas fa-download"></i>
{{ resource.likes || 0 }}
</span>
<span class="resource-date">{{
formatDate(resource.create_time)
}}</span>
</div>
</div>
</div>
</div>
<!-- 分页组件 -->
<div v-if="total > 0" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { ElMessage } from "element-plus";
// Props
interface ResourceConfig {
title: string;
description: string;
routePath: string;
api: {
getCategory: () => Promise<any>;
getSimpleLists: (cateid: string) => Promise<any>;
};
}
const props = defineProps<{
config: ResourceConfig;
}>();
//
const categories = ref<any[]>([]); //
const resources = ref<any[]>([]); //
const selectedCategory = ref<string>(""); //
const loading = ref(false); //
const currentPage = ref(1); //
const pageSize = ref(10); //
const total = ref(0); //
//
const currentCategoryName = computed(() => {
const category = categories.value.find(cat => cat.id === selectedCategory.value);
return category?.name || '';
});
//
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png";
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
//
const fetchCategories = async () => {
try {
const response: any = await props.config.api.getCategory();
if (
response.data &&
response.data.data &&
Array.isArray(response.data.data)
) {
categories.value = response.data.data;
//
if (categories.value.length > 0) {
selectedCategory.value = categories.value[0].id;
fetchResources();
}
} else {
categories.value = [];
ElMessage.warning(response.data?.msg || response.msg || "获取分类失败");
}
} catch (error) {
console.error("获取分类失败:", error);
categories.value = [];
ElMessage.warning("获取分类失败");
}
};
//
const fetchResources = async (page: number = currentPage.value) => {
loading.value = true;
try {
const response: any = await props.config.api.getSimpleLists(selectedCategory.value);
const data = response.data?.data;
if (data?.resources && Array.isArray(data.resources)) {
resources.value = data.resources;
total.value = data.total || 0;
currentPage.value = data.page || 1;
pageSize.value = data.limit || 10;
} else {
resources.value = [];
total.value = 0;
ElMessage.warning(response.data?.msg || "获取文章失败");
}
} catch (error) {
console.error("获取文章失败:", error);
resources.value = [];
total.value = 0;
ElMessage.warning("获取文章失败");
} finally {
loading.value = false;
}
};
//
const selectCategory = (categoryId: string) => {
selectedCategory.value = categoryId;
currentPage.value = 1; //
fetchResources(1);
};
//
const handlePageChange = (page: number) => {
currentPage.value = page;
fetchResources(page);
};
//
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; //
fetchResources(1);
};
//
const formatDate = (dateTime: string | number) => {
if (!dateTime) return "";
if (typeof dateTime === "string") {
//
return dateTime.split(' ')[0];
}
//
const date = new Date(dateTime < 1e10 ? dateTime * 1000 : dateTime);
return date.toLocaleDateString("zh-CN");
};
//
onMounted(() => {
fetchCategories();
});
</script>
<style scoped lang="less">
.resources-page {
padding-top: 100px;
min-height: 100vh;
background: #f9fafc;
.top-content {
background: linear-gradient(135deg, #1e9fff 0%, #0d8aff 100%);
padding: 80px 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--white);
.page-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 20px;
text-align: center;
}
.content {
line-height: 1.6;
p {
margin-bottom: 16px;
}
}
}
.main-content {
width: 1200px;
margin: 0 auto;
display: flex;
gap: 30px;
padding: 40px 0;
align-items: flex-start;
.left-menu {
background-color: var(--white);
padding: 20px;
border-radius: 8px;
width: 250px;
flex-shrink: 0;
align-self: flex-start;
.menu-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #1e9fff;
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
.category-item {
padding: 12px 16px;
margin-bottom: 8px;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
border-left: 3px solid transparent;
&:hover {
background: #f0f8ff;
border-left-color: #1e9fff;
}
&.active {
background: #e6f7ff;
border-left-color: #1e9fff;
color: #1e9fff;
font-weight: 500;
}
}
}
}
.contents {
background-color: var(--white);
// min-height: 400px;
border-radius: 8px;
padding: 20px;
flex: 1;
.pagination-wrapper {
margin-top: 30px;
display: flex;
justify-content: center;
padding: 20px 0;
border-top: 1px solid #e8e8e8;
.el-pagination {
--el-pagination-font-size: 14px;
--el-pagination-button-width: 40px;
--el-pagination-button-height: 40px;
}
}
.loading,
.no-data {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 16px;
}
.resources-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
.resource-card {
// min-width: 250px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.resource-image {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 140px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.card-header {
padding: 16px 16px 12px;
.resource-title-link {
text-decoration: none;
display: block;
&:hover .resource-title {
color: #007bff;
}
}
.resource-title {
font-size: 14px;
font-weight: 600;
height: 40px;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
transition: color 0.3s ease;
cursor: pointer;
}
.resource-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
.resource-view,
.resource-likes,
.resource-date {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.card-body {
padding: 0 16px;
.resource-summary {
font-size: 12px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}
}
.card-footer {
padding: 12px 16px 16px;
text-align: right;
.read-more {
color: #1e9fff;
text-decoration: none;
font-size: 12px;
font-weight: 500;
transition: color 0.3s ease;
&:hover {
color: #0d8aff;
text-decoration: underline;
}
}
}
}
}
}
}
}
</style>

View File

@ -1,24 +1,28 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useRoute } from "vue-router";
import { ref, onMounted, computed, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import Header from "@/views/components/header.vue";
import Footer from "@/views/components/footer.vue";
import { resource as resourceApi } from "@/api/resource";
const route = useRoute();
const router = useRouter(); // useRouter
const resourceId = ref(route.query.id as string);
const resourceData = ref<any>(null);
const prevResource = ref<any>(null);
const nextResource = ref<any>(null);
const relatedResources = ref<any[]>([]);
const loading = ref(true);
const showImageModal = ref(false);
const modalImageSrc = ref('');
const modalImageSrc = ref("");
const imageScale = ref(1);
const imageRotation = ref(0);
//
const pageInfo = computed(() => ({
path: (route.query.path as string) || '',
name: (route.query.category as string) || ''
path: (route.query.path as string) || "",
name: (route.query.category as string) || "",
}));
//
@ -27,17 +31,51 @@ const getImageUrl = (imagePath: string) => {
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
// contentURL
const processContentImages = (content: string) => {
if (!content) return "";
const apiDomain = import.meta.env.VITE_API_DOMAIN;
// URLdiv
return content.replace(
/<img([^>]+)src=["']([^"']+)["']([^>]*?)>/gi,
(_match, beforeSrc, src, afterSrc) => {
// URLhttphttps
if (src.startsWith("http://") || src.startsWith("https://")) {
return `<div class="content-image-wrapper"><img${beforeSrc}src="${src}"${afterSrc} data-clickable="true" style="width: 100%; height: auto; display: block; box-sizing: border-box; object-fit: fill; transition: opacity 0.3s; cursor: pointer;" onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'"></div>`;
}
//
const fullSrc = apiDomain + src;
return `<div class="content-image-wrapper"><img${beforeSrc}src="${fullSrc}"${afterSrc} data-clickable="true" style="width: 100%; height: auto; display: block; box-sizing: border-box; object-fit: fill; transition: opacity 0.3s; cursor: pointer;" onmouseover="this.style.opacity='0.8'" onmouseout="this.style.opacity='1'"></div>`;
}
);
};
// content
const handleContentClick = (event: Event) => {
const target = event.target as HTMLElement;
if (target.tagName === "IMG" && target.hasAttribute("data-clickable")) {
const imgSrc = target.getAttribute("src");
if (imgSrc) {
openImageModal(imgSrc);
}
}
};
//
const downloadFile = (downloadPath: string, type: string) => {
if (!downloadPath) return ElMessage.warning(`${type}下载链接不存在`);
const fullUrl = type === '本地' ? import.meta.env.VITE_API_DOMAIN + downloadPath : downloadPath;
const fullUrl =
type === "本地"
? import.meta.env.VITE_API_DOMAIN + downloadPath
: downloadPath;
try {
const link = document.createElement('a');
const link = document.createElement("a");
link.href = fullUrl;
link.target = '_blank';
link.download = '';
link.target = "_blank";
link.download = "";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@ -62,7 +100,7 @@ const copyShareCode = async () => {
textArea.value = code;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.execCommand("copy");
document.body.removeChild(textArea);
ElMessage.success("分享码已复制到剪贴板");
}
@ -80,14 +118,17 @@ const closeImageModal = () => {
};
const resetImageState = () => {
modalImageSrc.value = '';
modalImageSrc.value = "";
imageScale.value = 1;
imageRotation.value = 0;
};
const handleWheel = (event: WheelEvent) => {
event.preventDefault();
imageScale.value = Math.max(0.1, Math.min(3, imageScale.value + (event.deltaY > 0 ? -0.1 : 0.1)));
imageScale.value = Math.max(
0.1,
Math.min(3, imageScale.value + (event.deltaY > 0 ? -0.1 : 0.1))
);
};
const rotateImage = () => {
@ -99,58 +140,86 @@ const formatDate = (dateTime: string | number) => {
if (!dateTime) return "";
if (typeof dateTime === "string") {
//
return dateTime.split(' ')[0];
return dateTime.split(" ")[0];
}
//
const date = new Date(dateTime < 1e10 ? dateTime * 1000 : dateTime);
return date.toLocaleDateString("zh-CN");
};
//
//
const fetchResourceDetail = async () => {
if (!resourceId.value) return;
//
loading.value = true;
try {
// downloadGames使downloadGames API
const response:any = await resourceApi.getResourceDetail(resourceId.value);
resourceData.value = null;
prevResource.value = null;
nextResource.value = null;
relatedResources.value = [];
if (response.data?.data) {
resourceData.value = response.data.data;
try {
const response: any = await resourceApi.getResourceDetail(resourceId.value);
console.log("获取资源详情响应:", response);
if (response?.data?.code === 0 && response?.data?.data) {
resourceData.value = response.data.data.resource;
prevResource.value = response.data.data.prev_resource;
nextResource.value = response.data.data.next_resource;
relatedResources.value = response.data.data.related_resources || [];
console.log("资源数据加载完成:", resourceData.value?.title);
} else {
ElMessage.warning(response.data?.msg || "获取资源详情失败");
console.error("API响应格式错误:", response);
ElMessage.error("获取资源详情失败:数据格式错误");
}
} catch (error) {
console.error("API请求失败:", error);
ElMessage.error("获取资源详情失败,请稍后重试");
console.error("获取资源详情失败:", error);
} finally {
loading.value = false;
//
window.scrollTo({
top: 0,
behavior: "smooth", //
});
}
};
//
watch(
() => route.query.id,
(newId) => {
if (newId) {
resourceId.value = newId as string;
fetchResourceDetail(); //
}
},
{ immediate: true } //
);
onMounted(() => {
fetchResourceDetail();
// fetchResourceDetailwatch
//
window.scrollTo(0, 0);
});
</script>
<template>
<Header />
<div class="main-container">
<div class="container">
<div class="content" v-if="resourceData">
<div class="content">
<div class="top-content">
<div class="top-content-main">
<div class="top-content-main-title">{{ resourceData.title }}</div>
<div class="top-content-main-title">
{{ resourceData?.title || "加载中..." }}
</div>
<div class="location">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }"
>首页</el-breadcrumb-item
>
<el-breadcrumb-item :to="{ path: pageInfo.path }"
>{{ pageInfo.name }}</el-breadcrumb-item
>
<el-breadcrumb-item :to="{ path: pageInfo.path }">{{
pageInfo.name
}}</el-breadcrumb-item>
<el-breadcrumb-item>详情</el-breadcrumb-item>
</el-breadcrumb>
</div>
@ -162,60 +231,92 @@ onMounted(() => {
<div class="card-content-left">
<img
class="img-cover"
:src="getImageUrl(resourceData.icon)"
:alt="resourceData.title"
@click="openImageModal(getImageUrl(resourceData.icon))"
style="cursor: pointer;"
:src="getImageUrl(resourceData?.icon)"
:alt="resourceData?.title"
@click="openImageModal(getImageUrl(resourceData?.icon))"
style="cursor: pointer"
/>
</div>
<div class="card-content-right">
<div class="top-btns">
<button class="btn btn-primary" id="collectBtn"><i class="fa-solid fa-heart"></i> 收藏</button>
<button class="btn btn-primary" id="reportBtn" style="margin-left: 20px"><i class="fa-solid fa-flag"></i> 举报</button>
<button class="btn btn-primary" id="collectBtn">
<i class="fa-solid fa-heart"></i> 收藏
</button>
<button
class="btn btn-primary"
id="reportBtn"
style="margin-left: 20px"
>
<i class="fa-solid fa-flag"></i> 举报
</button>
</div>
<div class="resource-info">
<div class="title">
{{
resourceData.price == 0 || !resourceData.price
resourceData?.price == 0 || !resourceData?.price
? "Free"
: "¥" + resourceData.price
: "¥" + resourceData?.price
}}
</div>
<div class="infos">
<div class="infos-item">
<div class="label">更新时间</div>
<div class="value">{{ formatDate(resourceData.update_time || resourceData.create_time) }}</div>
<div class="value">
{{
formatDate(
resourceData?.update_time ||
resourceData?.create_time
)
}}
</div>
</div>
<div class="infos-item">
<div class="label">所属分类</div>
<div class="value">{{ resourceData.cate }}</div>
<div class="value">{{ resourceData?.cate }}</div>
</div>
<div class="infos-item">
<div class="label">程序编号</div>
<div class="value">{{ resourceData.number }}</div>
<div class="value">{{ resourceData?.number }}</div>
</div>
<div class="infos-item">
<div class="label">查看次数</div>
<div class="value">{{ resourceData.views }}</div>
<div class="value">{{ resourceData?.views }}</div>
</div>
<div class="infos-item">
<div class="label">下载次数</div>
<div class="value">{{ resourceData.downloads }}</div>
<div class="value">{{ resourceData?.downloads }}</div>
</div>
</div>
</div>
<div class="bottom-btns">
<button v-if="resourceData.url" id="netdiskBtn" class="btn btn-primary" @click="downloadFile(resourceData.url, '网盘')">
<button
v-if="resourceData?.url"
id="netdiskBtn"
class="btn btn-primary"
@click="downloadFile(resourceData?.url, '网盘')"
>
<i class="fa-solid fa-download"></i> 网盘下载
</button>
<button v-if="resourceData.fileurl" id="localBtn" class="btn btn-primary" @click="downloadFile(resourceData.fileurl, '本地')">
<button
v-if="resourceData?.fileurl"
id="localBtn"
class="btn btn-primary"
@click="downloadFile(resourceData?.fileurl, '本地')"
>
<i class="fa-solid fa-download"></i> 本地下载
</button>
<button v-if="resourceData.code" id="codeBtn" class="codebtn" @click="copyShareCode">
<i class="fa-solid fa-download"></i> 分享码{{resourceData.code}}
<button
v-if="resourceData?.code"
id="codeBtn"
class="codebtn"
@click="copyShareCode"
>
<i class="fa-solid fa-download"></i> 分享码{{
resourceData?.code
}}
</button>
</div>
</div>
@ -225,7 +326,124 @@ onMounted(() => {
<div class="resource-detail" v-if="resourceData">
<div class="resource-detail-title">详情介绍</div>
<el-divider />
<div class="resource-content" v-html="resourceData.content"></div>
<div
class="resource-content"
v-html="processContentImages(resourceData?.content)"
@click="handleContentClick"
></div>
<div class="disclaimers">
<div class="disclaimer-item">
<div class="disclaimer-title">免责声明:</div>
<div class="disclaimer-content">
<p>
本站非盈利性站点所有资源仅供学习参考并不贩卖软件不存在任何商业目的及用途如果您访问和下载此文件表示您同意只将此文件用于参考学习而非其他用途
</p>
<p>
本站所发布的一切软件资源均来自于互联网仅限用于学习和研究目的
</p>
<p>
不得将上述内容用于商业或者非法用途否则一切后果请用户自负
</p>
<p>本站所有软件信息来自网络版权争议与本站无关</p>
<p>
您必须在下载后的24个小时之内从您的电脑中彻底删除如果您喜欢该程序请支持正版软件购买注册得到更好的正版服务
</p>
<p>
如有侵权请邮件与我们联系处理我们会及时处理
邮箱yunzer_cn#163.com#换成@
</p>
</div>
</div>
<el-divider />
<div class="actions">
<div style="display: flex; gap: 30px">
<!-- 修复移除多余的} -->
<!-- 网盘下载按钮 -->
<button
v-if="resourceData?.url"
id="netdiskBtn"
class="btn btn-primary"
@click="downloadFile(resourceData?.url, '网盘')"
>
<i class="fa-solid fa-download"></i>
网盘下载
</button>
<!-- 本地下载按钮 -->
<button
v-if="resourceData?.fileurl"
id="localBtn"
class="btn btn-primary"
@click="downloadFile(resourceData?.fileurl, '本地')"
>
<i class="fa-solid fa-download"></i>
本地下载
</button>
<!-- 分享码按钮 -->
<button
v-if="resourceData?.code"
id="codeBtn"
class="codebtn"
@click="copyShareCode"
>
<i class="fa-solid fa-download"></i>
分享码{{ resourceData?.code }}
</button>
</div>
</div>
<el-divider />
<div class="navigation">
<div v-if="prevResource" class="prev" id="prev">
<a :href="`/index/resource/detail?id=${prevResource.id}`">
<i class="fa fa-arrow-left"></i>
上一篇{{ prevResource.title }}
</a>
</div>
<div v-if="nextResource" class="next" id="next">
<a :href="`/index/resource/detail?id=${nextResource.id}`">
下一篇{{ nextResource.title }}
<i class="fa fa-arrow-right"></i>
</a>
</div>
</div>
</div>
<div class="related-resources">
<h3>相关资源</h3>
<el-divider />
<div class="resources-list">
<div
v-for="resource in relatedResources"
:key="resource.id"
class="resource-item"
>
<div class="item-cover">
<img
:src="getImageUrl(resource.icon)"
:alt="resource.title"
/>
</div>
<div class="item-info">
<router-link
:to="{
path: '/resource',
query: {
id: resource.id,
path: pageInfo.path,
category: pageInfo.name,
},
}"
class="resource-title-link"
>
<h4 class="resource-title">
{{ resource.title }}
</h4>
</router-link>
</div>
</div>
</div>
</div>
</div>
<div class="loading" v-else-if="loading">
@ -245,21 +463,39 @@ onMounted(() => {
<div class="modal-content" @click.stop>
<!-- 控制按钮 -->
<div class="image-controls">
<button class="control-btn" @click="resetImageState" title="重置"><i class="fas fa-expand"></i></button>
<button class="control-btn" @click="rotateImage" title="旋转"><i class="fas fa-redo"></i></button>
<button class="control-btn" @click="imageScale = Math.max(0.1, imageScale - 0.2)" title="缩小"><i class="fas fa-search-minus"></i></button>
<button class="control-btn" @click="resetImageState" title="重置">
<i class="fas fa-expand"></i>
</button>
<button class="control-btn" @click="rotateImage" title="旋转">
<i class="fas fa-redo"></i>
</button>
<button
class="control-btn"
@click="imageScale = Math.max(0.1, imageScale - 0.2)"
title="缩小"
>
<i class="fas fa-search-minus"></i>
</button>
<span class="scale-display">{{ Math.round(imageScale * 100) }}%</span>
<button class="control-btn" @click="imageScale = Math.min(3, imageScale + 0.2)" title="放大"><i class="fas fa-search-plus"></i></button>
<button class="close-btn" @click="closeImageModal" title="关闭"><i class="fas fa-times"></i></button>
<button
class="control-btn"
@click="imageScale = Math.min(3, imageScale + 0.2)"
title="放大"
>
<i class="fas fa-search-plus"></i>
</button>
<button class="close-btn" @click="closeImageModal" title="关闭">
<i class="fas fa-times"></i>
</button>
</div>
<img
:src="modalImageSrc"
:alt="resourceData?.title"
:alt="resourceData?.title || '图片'"
class="modal-image"
:style="{
transform: `scale(${imageScale}) rotate(${imageRotation}deg)`,
maxWidth: '1200px',
maxHeight: '800px'
maxHeight: '800px',
}"
@wheel="handleWheel"
/>
@ -271,96 +507,297 @@ onMounted(() => {
<style lang="less" scoped>
.main-container {
padding-top: 100px; min-height: 100vh; background: #f9fafc;
padding-top: 100px;
min-height: 100vh;
background: #f9fafc;
.container {
width: 100%; margin: 0;
width: 100%;
margin: 0;
.content {
.top-content {
width: 100%; height: 400px; background-color: #0081ff; position: relative;
width: 100%;
height: 400px;
background-color: #0081ff;
position: relative;
.top-content-main {
max-width: 1200px; margin: 0 auto; padding-top: 50px;
display: flex; justify-content: space-between; position: absolute; top: 0;
left: 50%; transform: translateX(-50%); width: 100%; z-index: 1;
max-width: 1200px;
margin: 0 auto;
padding-top: 50px;
display: flex;
justify-content: space-between;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
z-index: 1;
.top-content-main-title { font-size: 30px; font-weight: 700; max-width: 1000px; color: #fff; }
.location { color: #fff; display: flex; align-items: center; }
.top-content-main-title {
font-size: 30px;
font-weight: 700;
max-width: 1000px;
color: #fff;
}
.location {
color: #fff;
display: flex;
align-items: center;
}
}
}
.main-content {
.info-card {
max-width: 1400px; margin: 0 auto; position: relative; background-color: #fff;
border-radius: 8px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); top: -150px;
max-width: 1400px;
margin: 0 auto;
position: relative;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
top: -150px;
.card-content {
display: flex; align-items: center; justify-content: space-between;
display: flex;
align-items: center;
justify-content: space-between;
.card-content-left {
padding: 20px;
.img-cover { border-radius: 8px; overflow: hidden; width: 450px; height: auto; background: cover; }
.img-cover {
border-radius: 8px;
overflow: hidden;
width: 450px;
height: auto;
background: cover;
}
}
.card-content-right {
padding: 20px; display: flex; flex-direction: column; gap: 30px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 30px;
.top-btns { display: flex; justify-content: flex-end; }
.top-btns {
display: flex;
justify-content: flex-end;
}
.resource-info {
display: flex; flex-direction: column;
display: flex;
flex-direction: column;
.title {
font-size: 35px; font-weight: 700; color: #42d697; margin-bottom: 15px;
font-size: 35px;
font-weight: 700;
color: #42d697;
margin-bottom: 15px;
}
.infos {
display: flex;
.infos-item {
margin-right: 60px; color: #7d879c; display: flex; flex-direction: column;
margin-right: 60px;
color: #7d879c;
display: flex;
flex-direction: column;
.label { margin-bottom: 10px; }
.label {
margin-bottom: 10px;
}
.value {
border: 1px #0d6efd dashed; padding: 3px 6px; font-size: 13px;
background-color: #0081ff12; border-radius: 5px;
border: 1px #0d6efd dashed;
padding: 3px 6px;
font-size: 13px;
background-color: #0081ff12;
border-radius: 5px;
}
}
}
}
.bottom-btns {
display: flex; align-items: center;
display: flex;
align-items: center;
.codebtn {
color: #0d6efd; margin-left: 20px; padding: 6px 20px; border-radius: 8px;
border: 1px solid #0d6efd; cursor: pointer; transition: all 0.3s ease;
color: #0d6efd;
margin-left: 20px;
padding: 6px 20px;
border-radius: 8px;
border: 1px solid #0d6efd;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fff;
}
}
}
}
}
.resource-detail {
position: relative;
top: -120px;
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 8px;
padding: 60px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
.resource-detail-title {
font-size: 2rem;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.resource-content {
width: 100%;
box-sizing: border-box;
line-height: 1.8;
color: #333;
font-size: 16px;
:deep(.content-image-wrapper) {
margin-bottom: 10px;
width: 100%;
box-sizing: border-box;
img {
width: 100%;
height: auto;
}
}
// .content-image-wrapper {
// margin-bottom: 10px;
// width: 100%;
// box-sizing: border-box;
// img{
// width: 100%;
// height: auto;
// }
// }
}
.disclaimers {
color: #b1b1b1;
width: 80%;
margin: 20px auto;
margin-bottom: 20px;
margin-bottom: 60px;
.disclaimer-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
}
.disclaimer-content {
font-size: 14px;
line-height: 1.6;
}
.actions {
display: flex;
justify-content: center;
#downloadBtn {
padding: 0 20px;
}
.codebtn {
color: #0d6efd;
padding: 15px 30px;
border-radius: 8px;
border: 1px solid #0d6efd;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fff;
}
}
.navigation {
display: flex;
justify-content: space-between;
margin: 30px 0;
a {
color: #333;
}
}
}
}
.related-resources {
margin: 40px 0;
.resources-list {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
.resource-item {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
&:hover {
transform: translateY(-5px); //
}
.resource-title-link {
text-decoration: none;
display: block;
&:hover .resource-title {
color: #007bff;
}
}
.resource-title {
font-size: 16px;
font-weight: 700;
color: #333;
margin-bottom: 15px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
transition: color 0.3s ease;
}
.item-cover {
margin-bottom: 20px;
img {
width: 100%;
height: 150px;
object-fit: cover;
}
}
.item-info {
padding: 10px;
}
}
}
}
}
}
}
}
.resource-detail {
position: relative; top: -120px; max-width: 1400px; margin: 0 auto;
background: white; border-radius: 8px; padding: 30px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
.resource-detail-title { font-size: 2rem; font-weight: bold; color: #333; margin-bottom: 20px; }
.resource-content {
line-height: 1.8; color: #333; font-size: 16px;
img { width: 100% !important; }
}
}
.loading,
.error {
margin-top: 50px;
@ -382,38 +819,85 @@ onMounted(() => {
//
.image-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center;
z-index: 9999; cursor: pointer;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
cursor: pointer;
.modal-content {
position: relative; cursor: default; display: flex; flex-direction: column; align-items: center;
position: relative;
cursor: default;
display: flex;
flex-direction: column;
align-items: center;
.image-controls {
display: flex; align-items: center; gap: 10px; margin-bottom: 20px;
background: rgba(255, 255, 255, 0.1); padding: 10px 20px; border-radius: 25px;
backdrop-filter: blur(10px); position: fixed; z-index: 9999; top: 90%;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.1);
padding: 10px 20px;
border-radius: 25px;
backdrop-filter: blur(10px);
position: fixed;
z-index: 9999;
top: 90%;
.control-btn, .close-btn {
background: rgba(255, 255, 255, 0.2); border: none; color: white;
width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
display: flex; align-items: center; justify-content: center; transition: all 0.3s ease;
.control-btn,
.close-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:hover { background: rgba(255, 255, 255, 0.3); transform: scale(1.1); }
i { font-size: 16px; }
&:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
i {
font-size: 16px;
}
}
.close-btn { margin-left: 10px; }
.close-btn {
margin-left: 10px;
}
.scale-display {
color: white; font-size: 14px; font-weight: bold; min-width: 50px; text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
min-width: 50px;
text-align: center;
}
}
.modal-image {
max-width: 1200px; max-height: 800px; object-fit: contain; border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease;
cursor: grab; &:active { cursor: grabbing; }
max-width: 1200px;
max-height: 800px;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease;
cursor: grab;
&:active {
cursor: grabbing;
}
}
}
}

View File

@ -1,437 +1,24 @@
<template>
<Header />
<div class="resources-page">
<div class="container">
<div class="top-content">
<h1 class="page-title">游戏资源</h1>
<div class="content flex flex-column align-items-center">
<p>发现优质游戏资源</p>
</div>
</div>
<div class="main-content">
<!-- 左侧分类导航 -->
<div class="left-menu">
<div class="menu-title">分类导航</div>
<ul class="category-list">
<li
v-for="category in categories"
:key="category.id"
class="category-item"
:class="{ active: selectedCategory === category.id }"
@click="selectCategory(category.id)"
>
{{ category.name }}
</li>
</ul>
</div>
<!-- 右侧内容区域 -->
<div class="contents">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="resources.length === 0" class="no-data">暂无内容</div>
<div v-else class="resources-grid">
<div
v-for="resource in resources"
:key="resource.id"
class="resource-card"
>
<div class="resource-image">
<img :src="getImageUrl(resource.icon)" :alt="resource.title" />
</div>
<div class="card-header">
<router-link
:to="`/resource?id=${resource.id}&path=/downloadGames&category=${encodeURIComponent(currentCategoryName)}`"
class="resource-title-link"
>
<h3 class="resource-title">{{ resource.title }}</h3>
</router-link>
<div class="resource-meta">
<span class="resource-view">
<i class="fa-solid fa-eye"></i>
{{ resource.view || 0 }}
</span>
<span class="resource-likes">
<i class="fas fa-download"></i>
{{ resource.likes || 0 }}
</span>
<span class="resource-date">{{
formatDate(resource.create_time)
}}</span>
</div>
</div>
</div>
</div>
<!-- 分页组件 -->
<div v-if="total > 0" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
</div>
<ResourceList :config="resourceConfig" />
<Footer />
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import Header from "@/views/components/header.vue";
import Footer from "@/views/components/footer.vue";
import ResourceList from "@/views/components/ResourceList.vue";
import { downloadGames } from "@/api/downloadGames";
//
const categories = ref<any[]>([]); //
const resources = ref<any[]>([]); //
const selectedCategory = ref<string>(""); //
const loading = ref(false); //
const currentPage = ref(1); //
const pageSize = ref(10); //
const total = ref(0); //
//
const currentCategoryName = computed(() => {
const category = categories.value.find(cat => cat.id === selectedCategory.value);
return category?.name || '';
});
//
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png";
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
//
const fetchCategories = async () => {
try {
const response: any = await downloadGames.getDownloadGamesCategory();
if (
response.data &&
response.data.data &&
Array.isArray(response.data.data)
) {
categories.value = response.data.data;
//
if (categories.value.length > 0) {
selectedCategory.value = categories.value[0].id;
fetchResources();
}
} else {
categories.value = [];
ElMessage.warning(response.data?.msg || response.msg || "获取分类失败");
}
} catch (error) {
console.error("获取分类失败:", error);
categories.value = [];
ElMessage.warning("获取分类失败");
//
const resourceConfig = {
title: "游戏资源",
description: "发现优质游戏资源",
routePath: "/downloadGames",
api: {
getCategory: downloadGames.getDownloadGamesCategory,
getSimpleLists: downloadGames.getDownloadGamesSimpleLists
}
};
//
const fetchResources = async (page: number = currentPage.value) => {
loading.value = true;
try {
const response: any = await downloadGames.getDownloadGamesSimpleLists(
selectedCategory.value
);
const data = response.data?.data;
if (data?.resources && Array.isArray(data.resources)) {
resources.value = data.resources;
total.value = data.total || 0;
currentPage.value = data.page || 1;
pageSize.value = data.limit || 10;
} else {
resources.value = [];
total.value = 0;
ElMessage.warning(response.data?.msg || "获取文章失败");
}
} catch (error) {
console.error("获取文章失败:", error);
resources.value = [];
total.value = 0;
ElMessage.warning("获取文章失败");
} finally {
loading.value = false;
}
};
//
const selectCategory = (categoryId: string) => {
selectedCategory.value = categoryId;
currentPage.value = 1; //
fetchResources(1);
};
//
const handlePageChange = (page: number) => {
currentPage.value = page;
fetchResources(page);
};
//
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; //
fetchResources(1);
};
//
const formatDate = (dateTime: string | number) => {
if (!dateTime) return "";
if (typeof dateTime === "string") {
//
return dateTime.split(' ')[0];
}
//
const date = new Date(dateTime < 1e10 ? dateTime * 1000 : dateTime);
return date.toLocaleDateString("zh-CN");
};
//
onMounted(() => {
fetchCategories();
});
</script>
<style scoped lang="less">
.resources-page {
padding-top: 100px;
min-height: 100vh;
background: #f9fafc;
.top-content {
background: linear-gradient(135deg, #1e9fff 0%, #0d8aff 100%);
padding: 80px 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--white);
.page-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 20px;
text-align: center;
}
.content {
line-height: 1.6;
p {
margin-bottom: 16px;
}
}
}
.main-content {
width: 1200px;
margin: 0 auto;
display: flex;
gap: 30px;
padding: 40px 0;
align-items: flex-start;
.left-menu {
background-color: var(--white);
padding: 20px;
border-radius: 8px;
width: 250px;
flex-shrink: 0;
align-self: flex-start;
.menu-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #1e9fff;
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
.category-item {
padding: 12px 16px;
margin-bottom: 8px;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
border-left: 3px solid transparent;
&:hover {
background: #f0f8ff;
border-left-color: #1e9fff;
}
&.active {
background: #e6f7ff;
border-left-color: #1e9fff;
color: #1e9fff;
font-weight: 500;
}
}
}
}
.contents {
background-color: var(--white);
// min-height: 400px;
border-radius: 8px;
padding: 20px;
flex: 1;
.pagination-wrapper {
margin-top: 30px;
display: flex;
justify-content: center;
padding: 20px 0;
border-top: 1px solid #e8e8e8;
.el-pagination {
--el-pagination-font-size: 14px;
--el-pagination-button-width: 40px;
--el-pagination-button-height: 40px;
}
}
.loading,
.no-data {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 16px;
}
.resources-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
.resource-card {
// min-width: 250px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.resource-image {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 140px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.card-header {
padding: 16px 16px 12px;
.resource-title-link {
text-decoration: none;
display: block;
&:hover .resource-title {
color: #007bff;
}
}
.resource-title {
font-size: 14px;
font-weight: 600;
height: 40px;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
transition: color 0.3s ease;
cursor: pointer;
}
.resource-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
.resource-view,
.resource-likes,
.resource-date {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.card-body {
padding: 0 16px;
.resource-summary {
font-size: 12px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}
}
.card-footer {
padding: 12px 16px 16px;
text-align: right;
.read-more {
color: #1e9fff;
text-decoration: none;
font-size: 12px;
font-weight: 500;
transition: color 0.3s ease;
&:hover {
color: #0d8aff;
text-decoration: underline;
}
}
}
}
}
}
}
}
</style>

View File

@ -1,437 +1,24 @@
<template>
<Header />
<div class="resources-page">
<div class="container">
<div class="top-content">
<h1 class="page-title">程序资源</h1>
<div class="content flex flex-column align-items-center">
<p>发现优质程序资源</p>
</div>
</div>
<div class="main-content">
<!-- 左侧分类导航 -->
<div class="left-menu">
<div class="menu-title">分类导航</div>
<ul class="category-list">
<li
v-for="category in categories"
:key="category.id"
class="category-item"
:class="{ active: selectedCategory === category.id }"
@click="selectCategory(category.id)"
>
{{ category.name }}
</li>
</ul>
</div>
<!-- 右侧内容区域 -->
<div class="contents">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="resources.length === 0" class="no-data">暂无内容</div>
<div v-else class="resources-grid">
<div
v-for="resource in resources"
:key="resource.id"
class="resource-card"
>
<div class="resource-image">
<img :src="getImageUrl(resource.icon)" :alt="resource.title" />
</div>
<div class="card-header">
<router-link
:to="`/resource?id=${resource.id}&path=/downloadPrograms&category=${encodeURIComponent(currentCategoryName)}`"
class="resource-title-link"
>
<h3 class="resource-title">{{ resource.title }}</h3>
</router-link>
<div class="resource-meta">
<span class="resource-view">
<i class="fa-solid fa-eye"></i>
{{ resource.view || 0 }}
</span>
<span class="resource-likes">
<i class="fas fa-download"></i>
{{ resource.likes || 0 }}
</span>
<span class="resource-date">{{
formatDate(resource.create_time)
}}</span>
</div>
</div>
</div>
</div>
<!-- 分页组件 -->
<div v-if="total > 0" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
</div>
<ResourceList :config="resourceConfig" />
<Footer />
</template>
<script setup lang="ts">
import { ref, onMounted,computed } from "vue";
import { ElMessage } from "element-plus";
import Header from "@/views/components/header.vue";
import Footer from "@/views/components/footer.vue";
import ResourceList from "@/views/components/ResourceList.vue";
import { downloadPrograms } from "@/api/downloadPrograms";
//
const categories = ref<any[]>([]); //
const resources = ref<any[]>([]); //
const selectedCategory = ref<string>(""); //
const loading = ref(false); //
const currentPage = ref(1); //
const pageSize = ref(10); //
const total = ref(0); //
//
const currentCategoryName = computed(() => {
const category = categories.value.find(cat => cat.id === selectedCategory.value);
return category?.name || '';
});
//
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png";
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
//
const fetchCategories = async () => {
try {
const response: any = await downloadPrograms.getDownloadProgramsCategory();
if (
response.data &&
response.data.data &&
Array.isArray(response.data.data)
) {
categories.value = response.data.data;
//
if (categories.value.length > 0) {
selectedCategory.value = categories.value[0].id;
fetchResources();
}
} else {
categories.value = [];
ElMessage.warning(response.data?.msg || response.msg || "获取分类失败");
}
} catch (error) {
console.error("获取分类失败:", error);
categories.value = [];
ElMessage.warning("获取分类失败");
//
const resourceConfig = {
title: "程序资源",
description: "发现优质程序资源",
routePath: "/downloadPrograms",
api: {
getCategory: downloadPrograms.getDownloadProgramsCategory,
getSimpleLists: downloadPrograms.getDownloadProgramsSimpleLists
}
};
//
const fetchResources = async (page: number = currentPage.value) => {
loading.value = true;
try {
const response: any = await downloadPrograms.getDownloadProgramsSimpleLists(
selectedCategory.value
);
const data = response.data?.data;
if (data?.resources && Array.isArray(data.resources)) {
resources.value = data.resources;
total.value = data.total || 0;
currentPage.value = data.page || 1;
pageSize.value = data.limit || 10;
} else {
resources.value = [];
total.value = 0;
ElMessage.warning(response.data?.msg || "获取文章失败");
}
} catch (error) {
console.error("获取文章失败:", error);
resources.value = [];
total.value = 0;
ElMessage.warning("获取文章失败");
} finally {
loading.value = false;
}
};
//
const selectCategory = (categoryId: string) => {
selectedCategory.value = categoryId;
currentPage.value = 1; //
fetchResources(1);
};
//
const handlePageChange = (page: number) => {
currentPage.value = page;
fetchResources(page);
};
//
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; //
fetchResources(1);
};
//
const formatDate = (dateTime: string | number) => {
if (!dateTime) return "";
if (typeof dateTime === "string") {
//
return dateTime.split(' ')[0];
}
//
const date = new Date(dateTime < 1e10 ? dateTime * 1000 : dateTime);
return date.toLocaleDateString("zh-CN");
};
//
onMounted(() => {
fetchCategories();
});
</script>
<style scoped lang="less">
.resources-page {
padding-top: 100px;
min-height: 100vh;
background: #f9fafc;
.top-content {
background: linear-gradient(135deg, #1e9fff 0%, #0d8aff 100%);
padding: 80px 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--white);
.page-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 20px;
text-align: center;
}
.content {
line-height: 1.6;
p {
margin-bottom: 16px;
}
}
}
.main-content {
width: 1200px;
margin: 0 auto;
display: flex;
gap: 30px;
padding: 40px 0;
align-items: flex-start;
.left-menu {
background-color: var(--white);
padding: 20px;
border-radius: 8px;
width: 250px;
flex-shrink: 0;
align-self: flex-start;
.menu-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #1e9fff;
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
.category-item {
padding: 12px 16px;
margin-bottom: 8px;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
border-left: 3px solid transparent;
&:hover {
background: #f0f8ff;
border-left-color: #1e9fff;
}
&.active {
background: #e6f7ff;
border-left-color: #1e9fff;
color: #1e9fff;
font-weight: 500;
}
}
}
}
.contents {
background-color: var(--white);
// min-height: 400px;
border-radius: 8px;
padding: 20px;
flex: 1;
.pagination-wrapper {
margin-top: 30px;
display: flex;
justify-content: center;
padding: 20px 0;
border-top: 1px solid #e8e8e8;
.el-pagination {
--el-pagination-font-size: 14px;
--el-pagination-button-width: 40px;
--el-pagination-button-height: 40px;
}
}
.loading,
.no-data {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 16px;
}
.resources-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
.resource-card {
// min-width: 250px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.resource-image {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 140px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.card-header {
padding: 16px 16px 12px;
.resource-title-link {
text-decoration: none;
display: block;
&:hover .resource-title {
color: #007bff;
}
}
.resource-title {
font-size: 14px;
font-weight: 600;
height: 40px;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
transition: color 0.3s ease;
cursor: pointer;
}
.resource-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
.resource-view,
.resource-likes,
.resource-date {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.card-body {
padding: 0 16px;
.resource-summary {
font-size: 12px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}
}
.card-footer {
padding: 12px 16px 16px;
text-align: right;
.read-more {
color: #1e9fff;
text-decoration: none;
font-size: 12px;
font-weight: 500;
transition: color 0.3s ease;
&:hover {
color: #0d8aff;
text-decoration: underline;
}
}
}
}
}
}
}
}
</style>

View File

@ -1,437 +1,24 @@
<template>
<Header />
<div class="resources-page">
<div class="container">
<div class="top-content">
<h1 class="page-title">办公资源</h1>
<div class="content flex flex-column align-items-center">
<p>发现优质程序与工具</p>
</div>
</div>
<div class="main-content">
<!-- 左侧分类导航 -->
<div class="left-menu">
<div class="menu-title">分类导航</div>
<ul class="category-list">
<li
v-for="category in categories"
:key="category.id"
class="category-item"
:class="{ active: selectedCategory === category.id }"
@click="selectCategory(category.id)"
>
{{ category.name }}
</li>
</ul>
</div>
<!-- 右侧内容区域 -->
<div class="contents">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="resources.length === 0" class="no-data">暂无内容</div>
<div v-else class="resources-grid">
<div
v-for="resource in resources"
:key="resource.id"
class="resource-card"
>
<div class="resource-image">
<img :src="getImageUrl(resource.icon)" :alt="resource.title" />
</div>
<div class="card-header">
<router-link
:to="`/resource?id=${resource.id}&path=/officeResources&category=${encodeURIComponent(currentCategoryName)}`"
class="resource-title-link"
>
<h3 class="resource-title">{{ resource.title }}</h3>
</router-link>
<div class="resource-meta">
<span class="resource-view">
<i class="fa-solid fa-eye"></i>
{{ resource.view || 0 }}
</span>
<span class="resource-likes">
<i class="fas fa-download"></i>
{{ resource.likes || 0 }}
</span>
<span class="resource-date">{{
formatDate(resource.create_time)
}}</span>
</div>
</div>
</div>
</div>
<!-- 分页组件 -->
<div v-if="total > 0" class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
</div>
<ResourceList :config="resourceConfig" />
<Footer />
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import Header from "@/views/components/header.vue";
import Footer from "@/views/components/footer.vue";
import ResourceList from "@/views/components/ResourceList.vue";
import { officeResources } from "@/api/officeResources";
//
const categories = ref<any[]>([]); //
const resources = ref<any[]>([]); //
const selectedCategory = ref<string>(""); //
const loading = ref(false); //
const currentPage = ref(1); //
const pageSize = ref(10); //
const total = ref(0); //
//
const currentCategoryName = computed(() => {
const category = categories.value.find(cat => cat.id === selectedCategory.value);
return category?.name || '';
});
//
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png";
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
//
const fetchCategories = async () => {
try {
const response: any = await officeResources.getOfficeResourcesCategory();
if (
response.data &&
response.data.data &&
Array.isArray(response.data.data)
) {
categories.value = response.data.data;
//
if (categories.value.length > 0) {
selectedCategory.value = categories.value[0].id;
fetchResources();
}
} else {
categories.value = [];
ElMessage.warning(response.data?.msg || response.msg || "获取分类失败");
}
} catch (error) {
console.error("获取分类失败:", error);
categories.value = [];
ElMessage.warning("获取分类失败");
//
const resourceConfig = {
title: "办公资源",
description: "发现优质程序与工具",
routePath: "/officeResources",
api: {
getCategory: officeResources.getOfficeResourcesCategory,
getSimpleLists: officeResources.getOfficeResourcesSimpleLists
}
};
//
const fetchResources = async (page: number = currentPage.value) => {
loading.value = true;
try {
const response: any = await officeResources.getOfficeResourcesSimpleLists(
selectedCategory.value
);
const data = response.data?.data;
if (data?.resources && Array.isArray(data.resources)) {
resources.value = data.resources;
total.value = data.total || 0;
currentPage.value = data.page || 1;
pageSize.value = data.limit || 10;
} else {
resources.value = [];
total.value = 0;
ElMessage.warning(response.data?.msg || "获取文章失败");
}
} catch (error) {
console.error("获取文章失败:", error);
resources.value = [];
total.value = 0;
ElMessage.warning("获取文章失败");
} finally {
loading.value = false;
}
};
//
const selectCategory = (categoryId: string) => {
selectedCategory.value = categoryId;
currentPage.value = 1; //
fetchResources(1);
};
//
const handlePageChange = (page: number) => {
currentPage.value = page;
fetchResources(page);
};
//
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; //
fetchResources(1);
};
//
const formatDate = (dateTime: string | number) => {
if (!dateTime) return "";
if (typeof dateTime === "string") {
//
return dateTime.split(' ')[0];
}
//
const date = new Date(dateTime < 1e10 ? dateTime * 1000 : dateTime);
return date.toLocaleDateString("zh-CN");
};
//
onMounted(() => {
fetchCategories();
});
</script>
<style scoped lang="less">
.resources-page {
padding-top: 100px;
min-height: 100vh;
background: #f9fafc;
.top-content {
background: linear-gradient(135deg, #1e9fff 0%, #0d8aff 100%);
padding: 80px 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--white);
.page-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 20px;
text-align: center;
}
.content {
line-height: 1.6;
p {
margin-bottom: 16px;
}
}
}
.main-content {
width: 1200px;
margin: 0 auto;
display: flex;
gap: 30px;
padding: 40px 0;
align-items: flex-start;
.left-menu {
background-color: var(--white);
padding: 20px;
border-radius: 8px;
width: 250px;
flex-shrink: 0;
align-self: flex-start;
.menu-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #1e9fff;
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
.category-item {
padding: 12px 16px;
margin-bottom: 8px;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
border-left: 3px solid transparent;
&:hover {
background: #f0f8ff;
border-left-color: #1e9fff;
}
&.active {
background: #e6f7ff;
border-left-color: #1e9fff;
color: #1e9fff;
font-weight: 500;
}
}
}
}
.contents {
background-color: var(--white);
// min-height: 400px;
border-radius: 8px;
padding: 20px;
flex: 1;
.pagination-wrapper {
margin-top: 30px;
display: flex;
justify-content: center;
padding: 20px 0;
border-top: 1px solid #e8e8e8;
.el-pagination {
--el-pagination-font-size: 14px;
--el-pagination-button-width: 40px;
--el-pagination-button-height: 40px;
}
}
.loading,
.no-data {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 16px;
}
.resources-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
.resource-card {
// min-width: 250px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.resource-image {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 140px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.card-header {
padding: 16px 16px 12px;
.resource-title-link {
text-decoration: none;
display: block;
&:hover .resource-title {
color: #007bff;
}
}
.resource-title {
font-size: 14px;
font-weight: 600;
height: 40px;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
transition: color 0.3s ease;
cursor: pointer;
}
.resource-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
.resource-view,
.resource-likes,
.resource-date {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.card-body {
padding: 0 16px;
.resource-summary {
font-size: 12px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}
}
.card-footer {
padding: 12px 16px 16px;
text-align: right;
.read-more {
color: #1e9fff;
text-decoration: none;
font-size: 12px;
font-weight: 500;
transition: color 0.3s ease;
&:hover {
color: #0d8aff;
text-decoration: underline;
}
}
}
}
}
}
}
}
</style>