优化程序界面

This commit is contained in:
扫地僧 2025-12-25 22:21:12 +08:00
parent fed42c7589
commit dd6ee001f1
17 changed files with 838 additions and 465 deletions

View File

@ -23,6 +23,7 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElTag: typeof import('element-plus/es')['ElTag']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

View File

@ -0,0 +1,13 @@
//进行接口API的统一管理
import { request } from "./axios";
export class article {
/**
* @description article文章详情
* @param {string} id - ID
* @return {Promise}
*/
static async getArticleDetail(id: string) {
return request("/index/articles/getArticleDetail", { id }, "get");
}
}

View File

@ -28,12 +28,4 @@ export class downloadGames {
return request("/index/program/getDownloadGamesSimpleLists", { cateid }, "get"); return request("/index/program/getDownloadGamesSimpleLists", { cateid }, "get");
} }
/**
* @description downloadGames文章详情
* @param {string} id - ID
* @return {Promise}
*/
static async getDownloadGamesDetail(id: string) {
return request("/index/program/getDownloadGamesDetail", { id }, "get");
}
} }

View File

@ -36,12 +36,4 @@ export class downloadPrograms {
); );
} }
/**
* @description downloadPrograms文章详情
* @param {string} id - ID
* @return {Promise}
*/
static async getDownloadProgramsDetail(id: string) {
return request("/index/program/getDownloadProgramsDetail", { id }, "get");
}
} }

View File

@ -32,12 +32,4 @@ export class officeResources {
); );
} }
/**
* @description officeResources文章详情
* @param {string} id - ID
* @return {Promise}
*/
static async getOfficeResourcesDetail(id: string) {
return request("/index/program/getOfficeResourcesDetail", { id }, "get");
}
} }

View File

@ -0,0 +1,13 @@
//进行接口API的统一管理
import { request } from "./axios";
export class resource {
/**
* @description resource文章详情
* @param {string} id - ID
* @return {Promise}
*/
static async getResourceDetail(id: string) {
return request("/index/resources/getResourceDetail", { id }, "get");
}
}

View File

@ -18,4 +18,5 @@ export class siteInformation {
static async getSiteInformationLists(cateid: string) { static async getSiteInformationLists(cateid: string) {
return request("/index/articles/getSiteInformationLists", { cateid }, "get"); return request("/index/articles/getSiteInformationLists", { cateid }, "get");
} }
} }

View File

@ -22,4 +22,5 @@ export class technicalArticles {
"get" "get"
); );
} }
} }

View File

@ -33,9 +33,14 @@ const router = createRouter({
component: () => import("@/views/downloadPrograms/index.vue"), component: () => import("@/views/downloadPrograms/index.vue"),
}, },
{ {
path: "/downloadPrograms/:id", path: "/article",
name: "downloadProgramsDetail", name: "articleDetail",
component: () => import("@/views/downloadPrograms/detail.vue"), component: () => import("@/views/components/detail_article.vue"),
},
{
path: "/resource",
name: "resourceDetail",
component: () => import("@/views/components/detail_resource.vue"),
}, },
{ {
path: "/downloadGames", path: "/downloadGames",

View File

@ -0,0 +1,353 @@
<script setup lang="ts">
import { ref, onMounted } 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 { siteInformation } from "@/api/siteInformation";
import { technicalArticles } from "@/api/technicalArticles";
const route = useRoute();
const router = useRouter();
const articleId = ref(route.query.id as string);
const article = ref<any>(null);
const loading = ref(true);
//
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png";
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
//
const formatDate = (timestamp: string | number) => {
if (!timestamp) return "";
const numTimestamp = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
const date = new Date(numTimestamp < 1e10 ? numTimestamp * 1000 : numTimestamp);
return date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
};
//
const fetchArticleDetail = async () => {
if (!articleId.value) {
ElMessage.error("文章ID不存在");
router.push("/");
return;
}
loading.value = true;
try {
// API
const source = route.query.source as string;
let response: any;
if (source === 'siteInformation') {
response = await siteInformation.getSiteInformationDetail(articleId.value);
} else if (source === 'technicalArticles') {
response = await technicalArticles.getTechnicalArticlesDetail(articleId.value);
} else {
// API
try {
response = await siteInformation.getSiteInformationDetail(articleId.value);
} catch {
response = await technicalArticles.getTechnicalArticlesDetail(articleId.value);
}
}
if (response.data?.data) {
article.value = response.data.data;
} else {
ElMessage.warning(response.data?.msg || "获取文章详情失败");
router.push("/");
}
} catch (error) {
ElMessage.error("获取文章详情失败,请稍后重试");
console.error("获取文章详情失败:", error);
router.push("/");
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchArticleDetail();
});
</script>
<template>
<Header />
<div class="article-detail">
<div class="container">
<!-- 面包屑导航 -->
<div class="breadcrumb-wrapper" v-if="article">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/siteInformation' }">站点资讯</el-breadcrumb-item>
<el-breadcrumb-item>详情</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<el-icon class="is-loading">
<Loading />
</el-icon>
<span>加载中...</span>
</div>
<!-- 文章不存在 -->
<div v-else-if="!article" class="error">
<el-empty description="文章不存在或已删除"></el-empty>
</div>
<!-- 文章内容 -->
<div v-else class="article-content">
<!-- 文章标题 -->
<div class="article-header">
<h1 class="article-title">{{ article.title }}</h1>
<div class="article-meta">
<span class="article-date">
<i class="fas fa-calendar"></i>
{{ formatDate(article.publishdate || article.created_at) }}
</span>
<span class="article-views" v-if="article.view">
<i class="fas fa-eye"></i>
{{ article.view }} 阅读
</span>
<span class="article-likes" v-if="article.likes">
<i class="fas fa-heart"></i>
{{ article.likes }} 点赞
</span>
</div>
</div>
<!-- 文章图片 -->
<div class="article-image" v-if="article.image">
<img :src="getImageUrl(article.image)" :alt="article.title" />
</div>
<!-- 文章正文 -->
<div class="article-body">
<div
class="article-text"
v-html="article.content"
></div>
</div>
<!-- 文章标签 -->
<div class="article-tags" v-if="article.tags && article.tags.length">
<span class="tag-label">标签</span>
<el-tag
v-for="tag in article.tags"
:key="tag"
size="small"
class="article-tag"
>
{{ tag }}
</el-tag>
</div>
</div>
</div>
</div>
<Footer />
</template>
<style lang="less" scoped>
.article-detail {
padding-top: 100px;
min-height: 100vh;
background: #f9fafc;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.breadcrumb-wrapper {
margin-bottom: 30px;
:deep(.el-breadcrumb__inner) {
color: #fff !important;
font-weight: bolder !important;
}
a {
color: #666;
text-decoration: none;
&:hover {
color: #007bff;
}
}
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 0;
color: #666;
.el-icon {
font-size: 24px;
margin-bottom: 10px;
}
}
.error {
padding: 50px 0;
}
.article-content {
background: white;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.article-header {
border-bottom: 1px solid #eee;
padding-bottom: 20px;
margin-bottom: 30px;
}
.article-title {
font-size: 2.5rem;
font-weight: bold;
color: #333;
line-height: 1.3;
margin: 0 0 20px 0;
}
.article-meta {
display: flex;
gap: 20px;
color: #666;
font-size: 14px;
span {
display: flex;
align-items: center;
gap: 5px;
i {
font-size: 16px;
}
}
}
.article-image {
margin-bottom: 30px;
text-align: center;
img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
}
.article-body {
color: #333;
line-height: 1.8;
font-size: 16px;
}
.article-text {
:deep(p) {
margin-bottom: 16px;
line-height: 1.8;
}
:deep(img) {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 16px 0;
}
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
color: #333;
margin: 24px 0 16px 0;
font-weight: 600;
}
:deep(ul), :deep(ol) {
padding-left: 24px;
margin-bottom: 16px;
}
:deep(code) {
background: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
}
:deep(pre) {
background: #f6f8fa;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
margin: 16px 0;
code {
background: none;
padding: 0;
}
}
:deep(blockquote) {
border-left: 4px solid #ddd;
padding-left: 16px;
margin: 16px 0;
color: #666;
font-style: italic;
}
}
.article-tags {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
.tag-label {
color: #666;
font-size: 14px;
margin-right: 10px;
}
.article-tag {
margin-right: 8px;
margin-bottom: 8px;
}
}
@media (max-width: 768px) {
.container {
padding: 0 15px;
}
.article-content {
padding: 20px;
}
.article-title {
font-size: 2rem;
}
.article-meta {
flex-direction: column;
gap: 8px;
}
}
</style>

View File

@ -0,0 +1,420 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useRoute } 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 resourceId = ref(route.query.id as string);
const resourceData = ref<any>(null);
const loading = ref(true);
const showImageModal = ref(false);
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) || ''
}));
//
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png";
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
//
const downloadFile = (downloadPath: string, type: string) => {
if (!downloadPath) return ElMessage.warning(`${type}下载链接不存在`);
const fullUrl = type === '本地' ? import.meta.env.VITE_API_DOMAIN + downloadPath : downloadPath;
try {
const link = document.createElement('a');
link.href = fullUrl;
link.target = '_blank';
link.download = '';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success(`${type}下载链接已打开`);
} catch (error) {
ElMessage.error(`${type}下载失败,请稍后重试`);
console.error(`${type}下载失败:`, error);
}
};
//
const copyShareCode = async () => {
const code = resourceData.value?.code;
if (!code) return ElMessage.warning("分享码不存在");
try {
await navigator.clipboard.writeText(code);
ElMessage.success("分享码已复制到剪贴板");
} catch {
//
const textArea = document.createElement("textarea");
textArea.value = code;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
ElMessage.success("分享码已复制到剪贴板");
}
};
//
const openImageModal = (imageSrc: string) => {
modalImageSrc.value = imageSrc;
showImageModal.value = true;
};
const closeImageModal = () => {
showImageModal.value = false;
resetImageState();
};
const resetImageState = () => {
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)));
};
const rotateImage = () => {
imageRotation.value = (imageRotation.value + 90) % 360;
};
//
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");
};
//
const fetchResourceDetail = async () => {
if (!resourceId.value) return;
loading.value = true;
try {
// downloadGames使downloadGames API
const response = await resourceApi.getResourceDetail(resourceId.value);
if (response.data?.data) {
resourceData.value = response.data.data;
} else {
ElMessage.warning(response.data?.msg || "获取资源详情失败");
}
} catch (error) {
ElMessage.error("获取资源详情失败,请稍后重试");
console.error("获取资源详情失败:", error);
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchResourceDetail();
});
</script>
<template>
<Header />
<div class="main-container">
<div class="container">
<div class="content" v-if="resourceData">
<div class="top-content">
<div class="top-content-main">
<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>详情</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</div>
<div class="main-content">
<div class="info-card">
<div class="card-content">
<div class="card-content-left">
<img
class="img-cover"
: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>
</div>
<div class="resource-info">
<div class="title">
{{
resourceData.price == 0 || !resourceData.price
? "Free"
: "¥" + 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>
<div class="infos-item">
<div class="label">所属分类</div>
<div class="value">{{ resourceData.cate }}</div>
</div>
<div class="infos-item">
<div class="label">程序编号</div>
<div class="value">{{ resourceData.number }}</div>
</div>
<div class="infos-item">
<div class="label">查看次数</div>
<div class="value">{{ resourceData.views }}</div>
</div>
<div class="infos-item">
<div class="label">下载次数</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, '网盘')">
<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>
</div>
</div>
<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>
<div class="loading" v-else-if="loading">
<el-empty description="正在加载文章详情..."></el-empty>
</div>
<div class="error" v-else>
<el-empty description="文章不存在或已删除"></el-empty>
</div>
</div>
</div>
</div>
</div>
<!-- 图片放大模态框 -->
<div v-if="showImageModal" class="image-modal" @click="closeImageModal">
<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>
<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>
</div>
<img
:src="modalImageSrc"
:alt="resourceData?.title"
class="modal-image"
:style="{
transform: `scale(${imageScale}) rotate(${imageRotation}deg)`,
maxWidth: '1200px',
maxHeight: '800px'
}"
@wheel="handleWheel"
/>
</div>
</div>
<Footer />
</template>
<style lang="less" scoped>
.main-container {
padding-top: 100px; min-height: 100vh; background: #f9fafc;
.container {
width: 100%; margin: 0;
.content {
.top-content {
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;
.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;
.card-content {
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; }
}
.card-content-right {
padding: 20px; display: flex; flex-direction: column; gap: 30px;
.top-btns { display: flex; justify-content: flex-end; }
.resource-info {
display: flex; flex-direction: column;
.title {
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;
.label { margin-bottom: 10px; }
.value {
border: 1px #0d6efd dashed; padding: 3px 6px; font-size: 13px;
background-color: #0081ff12; border-radius: 5px;
}
}
}
}
.bottom-btns {
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;
background-color: #fff;
}
}
}
}
}
}
}
}
}
.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;
}
:deep(.el-breadcrumb__inner) {
color: #fff !important;
font-weight: bolder !important;
a,
&.is-link {
color: #fff !important;
}
}
:deep(.el-breadcrumb__separator) {
color: #fff !important;
}
//
.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;
.modal-content {
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%;
.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; }
}
.close-btn { margin-left: 10px; }
.scale-display {
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; }
}
}
}
</style>

View File

@ -3,9 +3,9 @@
<div class="resources-page"> <div class="resources-page">
<div class="container"> <div class="container">
<div class="top-content"> <div class="top-content">
<h1 class="page-title">办公资源</h1> <h1 class="page-title">游戏资源</h1>
<div class="content flex flex-column align-items-center"> <div class="content flex flex-column align-items-center">
<p>发现优质程序与工具</p> <p>发现优质游戏资源</p>
</div> </div>
</div> </div>
<div class="main-content"> <div class="main-content">
@ -40,7 +40,7 @@
</div> </div>
<div class="card-header"> <div class="card-header">
<router-link <router-link
:to="`/resource/${resource.id}`" :to="`/resource?id=${resource.id}&path=/downloadGames&category=${encodeURIComponent(currentCategoryName)}`"
class="resource-title-link" class="resource-title-link"
> >
<h3 class="resource-title">{{ resource.title }}</h3> <h3 class="resource-title">{{ resource.title }}</h3>
@ -97,6 +97,12 @@ const currentPage = ref(1); // 当前页码
const pageSize = ref(10); // const pageSize = ref(10); //
const total = ref(0); // const total = ref(0); //
//
const currentCategoryName = computed(() => {
const category = categories.value.find(cat => cat.id === selectedCategory.value);
return category?.name || '';
});
// //
const getImageUrl = (imagePath: string) => { const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png"; if (!imagePath) return "/src/assets/imgs/default.png";

View File

@ -1,428 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import Header from "@/views/components/header.vue";
import Footer from "@/views/components/footer.vue";
import { downloadPrograms } from "@/api/downloadPrograms";
const route = useRoute();
const resourceId = ref(route.query.id as string);
const resource = ref<any>(null);
const loading = ref(true);
//
const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png";
return import.meta.env.VITE_API_DOMAIN + imagePath;
};
//
const downloadFile = (downloadPath: string, type: string) => {
if (!downloadPath) {
ElMessage.warning(`${type}下载链接不存在`);
return;
}
let fullUrl = downloadPath;
// URL
if (type === '本地') {
fullUrl = import.meta.env.VITE_API_DOMAIN + downloadPath;
}
try {
// a
const link = document.createElement('a');
link.href = fullUrl;
link.target = '_blank';
link.download = ''; //
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
ElMessage.success(`${type}下载链接已打开`);
} catch (error) {
ElMessage.error(`${type}下载失败,请稍后重试`);
console.error(`${type}下载失败:`, error);
}
};
//
const copyShareCode = async () => {
if (!resource.value?.code) {
ElMessage.warning("分享码不存在");
return;
}
try {
await navigator.clipboard.writeText(resource.value.code);
ElMessage.success("分享码已复制到剪贴板");
} catch (error) {
// clipboard API
const textArea = document.createElement("textarea");
textArea.value = resource.value.code;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
ElMessage.success("分享码已复制到剪贴板");
}
};
onMounted(async () => {
if (resourceId.value) {
loading.value = true;
try {
const response: any = await downloadPrograms.getDownloadProgramsDetail(
resourceId.value
);
if (response.data?.data) {
resource.value = response.data.data;
} else {
ElMessage.warning(response.data?.msg || "获取文章详情失败");
}
} catch (error) {
ElMessage.error("获取文章详情失败,请稍后重试");
console.error("获取文章详情失败:", error);
} finally {
loading.value = false;
}
}
});
</script>
<template>
<Header />
<div class="main-container">
<div class="container">
<div class="content" v-if="resource">
<div class="top-content">
<div class="top-content-main">
<div class="top-content-main-title">{{ resource.title }}</div>
<div class="location">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }"
>首页</el-breadcrumb-item
>
<el-breadcrumb-item :to="{ path: '/downloadPrograms' }"
>程序下载</el-breadcrumb-item
>
<el-breadcrumb-item>详情</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</div>
<div class="main-content">
<div class="info-card">
<div class="card-content">
<div class="card-content-left">
<img
class="img-cover"
:src="getImageUrl(resource.icon)"
:alt="resource.title"
/>
</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>
</div>
<div class="resource-info">
<div class="title">
{{
resource.price == 0 || !resource.price
? "Free"
: "¥" + resource.price
}}
</div>
<div class="infos">
<div class="infos-item">
<div class="label">更新时间</div>
<div class="value">{{ resource.update_time }}</div>
</div>
<div class="infos-item">
<div class="label">所属分类</div>
<div class="value">{{ resource.cate }}</div>
</div>
<div class="infos-item">
<div class="label">程序编号</div>
<div class="value">{{ resource.number }}</div>
</div>
<div class="infos-item">
<div class="label">查看次数</div>
<div class="value">{{ resource.views }}</div>
</div>
<div class="infos-item">
<div class="label">下载次数</div>
<div class="value">{{ resource.downloads }}</div>
</div>
</div>
</div>
<div class="bottom-btns">
<!-- 网盘下载按钮 -->
<button
v-if="resource.url"
id="netdiskBtn"
class="btn btn-primary"
@click="downloadFile(resource.url, '网盘')"
>
<i class="fa-solid fa-download"></i>
网盘下载
</button>
<!-- 本地下载按钮 -->
<button
v-if="resource.fileurl"
id="localBtn"
class="btn btn-primary"
@click="downloadFile(resource.fileurl, '本地')"
>
<i class="fa-solid fa-download"></i>
本地下载
</button>
<!-- 分享码按钮 -->
<button
v-if="resource.code"
id="codeBtn"
class="codebtn"
@click="copyShareCode"
>
<i class="fa-solid fa-download"></i>
分享码{{resource.code}}
</button>
</div>
</div>
</div>
</div>
<div class="resource-detail" v-if="resource">
<div class="resource-content" v-html="resource.content"></div>
</div>
<div class="loading" v-else-if="loading">
<el-empty description="正在加载文章详情..."></el-empty>
</div>
<div class="error" v-else>
<el-empty description="文章不存在或已删除"></el-empty>
</div>
</div>
</div>
</div>
</div>
<Footer />
</template>
<style lang="less" scoped>
.main-container {
padding-top: 100px;
min-height: 100vh;
background: #f9fafc;
.container {
width: 100%;
margin: 0;
.content {
.top-content {
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;
.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: 1200px;
/* height: 300px; */
margin: 0px 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;
.card-content-left {
padding: 20px;
.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;
.top-btns {
display: flex;
justify-content: flex-end;
}
.resource-info {
display: flex;
flex-direction: column;
.title {
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;
.label {
margin-bottom: 10px;
}
.value {
border: 1px #0d6efd dashed;
padding: 3px 6px;
font-size: 13px;
background-color: #0081ff12;
border-radius: 5px;
}
}
}
}
.bottom-btns {
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;
background-color: #fff;
}
}
}
}
}
}
}
}
}
.resource-detail {
position: relative;
top: -120px;
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
.resource-title {
font-size: 28px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.resource-meta {
display: flex;
gap: 20px;
margin-bottom: 30px;
color: #666;
font-size: 14px;
span {
display: flex;
align-items: center;
gap: 5px;
}
}
.resource-content {
line-height: 1.8;
color: #333;
font-size: 16px;
}
}
.loading,
.error {
margin-top: 50px;
}
:deep(.el-breadcrumb__inner) {
color: #fff !important;
font-weight: bolder !important;
a,
&.is-link {
color: #fff !important;
}
}
:deep(.el-breadcrumb__separator) {
color: #fff !important;
}
</style>

View File

@ -3,9 +3,9 @@
<div class="resources-page"> <div class="resources-page">
<div class="container"> <div class="container">
<div class="top-content"> <div class="top-content">
<h1 class="page-title">办公资源</h1> <h1 class="page-title">程序资源</h1>
<div class="content flex flex-column align-items-center"> <div class="content flex flex-column align-items-center">
<p>发现优质程序与工具</p> <p>发现优质程序资源</p>
</div> </div>
</div> </div>
<div class="main-content"> <div class="main-content">
@ -40,7 +40,7 @@
</div> </div>
<div class="card-header"> <div class="card-header">
<router-link <router-link
:to="`/downloadPrograms/detail?id=${resource.id}`" :to="`/resource?id=${resource.id}&path=/downloadPrograms&category=${encodeURIComponent(currentCategoryName)}`"
class="resource-title-link" class="resource-title-link"
> >
<h3 class="resource-title">{{ resource.title }}</h3> <h3 class="resource-title">{{ resource.title }}</h3>
@ -82,7 +82,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted,computed } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import Header from "@/views/components/header.vue"; import Header from "@/views/components/header.vue";
import Footer from "@/views/components/footer.vue"; import Footer from "@/views/components/footer.vue";
@ -97,6 +97,12 @@ const currentPage = ref(1); // 当前页码
const pageSize = ref(10); // const pageSize = ref(10); //
const total = ref(0); // const total = ref(0); //
//
const currentCategoryName = computed(() => {
const category = categories.value.find(cat => cat.id === selectedCategory.value);
return category?.name || '';
});
// //
const getImageUrl = (imagePath: string) => { const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png"; if (!imagePath) return "/src/assets/imgs/default.png";

View File

@ -40,7 +40,7 @@
</div> </div>
<div class="card-header"> <div class="card-header">
<router-link <router-link
:to="`/resource/${resource.id}`" :to="`/resource?id=${resource.id}&path=/officeResources&category=${encodeURIComponent(currentCategoryName)}`"
class="resource-title-link" class="resource-title-link"
> >
<h3 class="resource-title">{{ resource.title }}</h3> <h3 class="resource-title">{{ resource.title }}</h3>
@ -97,6 +97,12 @@ const currentPage = ref(1); // 当前页码
const pageSize = ref(10); // const pageSize = ref(10); //
const total = ref(0); // const total = ref(0); //
//
const currentCategoryName = computed(() => {
const category = categories.value.find(cat => cat.id === selectedCategory.value);
return category?.name || '';
});
// //
const getImageUrl = (imagePath: string) => { const getImageUrl = (imagePath: string) => {
if (!imagePath) return "/src/assets/imgs/default.png"; if (!imagePath) return "/src/assets/imgs/default.png";

View File

@ -39,7 +39,7 @@
<img :src="getImageUrl(article.image)" :alt="article.title"> <img :src="getImageUrl(article.image)" :alt="article.title">
</div> </div>
<div class="card-header"> <div class="card-header">
<router-link :to="`/article/${article.id}`" class="article-title-link"> <router-link :to="`/article?id=${article.id}&source=siteInformation`" class="article-title-link">
<h3 class="article-title">{{ article.title }}</h3> <h3 class="article-title">{{ article.title }}</h3>
</router-link> </router-link>
<div class="article-meta"> <div class="article-meta">

View File

@ -40,7 +40,7 @@
</div> </div>
<div class="card-header"> <div class="card-header">
<router-link <router-link
:to="`/article/${article.id}`" :to="`/article?id=${article.id}&source=technicalArticles`"
class="article-title-link" class="article-title-link"
> >
<h3 class="article-title">{{ article.title }}</h3> <h3 class="article-title">{{ article.title }}</h3>