对接文章接口
This commit is contained in:
parent
6a108992e2
commit
5efeb4cb89
|
|
@ -5,6 +5,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>装样大师</title>
|
<title>装样大师</title>
|
||||||
|
<meta name="keywords" content="装样大师, 模拟器" />
|
||||||
|
<meta name="description" content="装样大师是一款高保真的模拟工具" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
|
"marked": "^18.0.7",
|
||||||
"mime": "^4.1.0",
|
"mime": "^4.1.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
|
@ -2621,6 +2622,17 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "18.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz",
|
||||||
|
"integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
"start": "node --stack-size=12800 --stack-trace-limit=20 ./service/main.cjs"
|
"start": "node --stack-size=12800 --stack-trace-limit=20 ./service/main.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@batiao/batiao-sdk-vue-vite": "^1.0.357",
|
"@batiao/batiao-sdk-vue-vite": "^1.0.386",
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"body-parser": "^2.3.0",
|
"body-parser": "^2.3.0",
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
|
"marked": "^18.0.7",
|
||||||
"mime": "^4.1.0",
|
"mime": "^4.1.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ const routes = require('./routes.cjs')
|
||||||
const httpProxy = require('http-proxy')
|
const httpProxy = require('http-proxy')
|
||||||
const proxy = httpProxy.createProxyServer({})
|
const proxy = httpProxy.createProxyServer({})
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const url = require('url')
|
||||||
|
const axios = require('axios')
|
||||||
const dist = 'dist'
|
const dist = 'dist'
|
||||||
let mime
|
let mime
|
||||||
import('mime').then((res) => {
|
import('mime').then((res) => {
|
||||||
|
|
@ -105,11 +107,46 @@ const matchFile = async (request, response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const endIndex = async (request, response) => {
|
const endIndex = async (request, response) => {
|
||||||
console.log(dist + '/index.html')
|
fs.readFile(dist + '/index.html', 'utf-8', async (err, html) => {
|
||||||
fs.readFile(dist + '/index.html', (err, html) => {
|
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
let finalHtml = html
|
||||||
|
try {
|
||||||
|
const parsedUrl = url.parse(request.url, true)
|
||||||
|
if (parsedUrl.pathname === '/article-detail' && parsedUrl.query.id) {
|
||||||
|
const id = parsedUrl.query.id
|
||||||
|
const res = await axios.get(`http://wechat.wenyitu.com/api/articles/${id}`)
|
||||||
|
const article = res.data
|
||||||
|
if (article) {
|
||||||
|
if (article.title) {
|
||||||
|
finalHtml = finalHtml.replace(/<title>.*?<\/title>/, `<title>${article.title} - 装样大师</title>`)
|
||||||
|
}
|
||||||
|
if (article.seo_keywords) {
|
||||||
|
finalHtml = finalHtml.replace(/<meta name="keywords" content="[^"]*"/, `<meta name="keywords" content="${article.seo_keywords}"`)
|
||||||
|
}
|
||||||
|
if (article.seo_description) {
|
||||||
|
finalHtml = finalHtml.replace(/<meta name="description" content="[^"]*"/, `<meta name="description" content="${article.seo_description}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parsedUrl.pathname === '/article' && parsedUrl.query.category_id) {
|
||||||
|
const categoryId = parsedUrl.query.category_id
|
||||||
|
const res = await axios.get(`http://wechat.wenyitu.com/api/articles?category_id=${categoryId}&page=1&per_page=10`)
|
||||||
|
const data = res.data
|
||||||
|
if (data && data.items && data.items.length > 0 && data.items[0].category) {
|
||||||
|
const category = data.items[0].category
|
||||||
|
if (category.seo_keywords) {
|
||||||
|
finalHtml = finalHtml.replace(/<meta name="keywords" content="[^"]*"/, `<meta name="keywords" content="${category.seo_keywords}"`)
|
||||||
|
}
|
||||||
|
if (category.seo_description) {
|
||||||
|
finalHtml = finalHtml.replace(/<meta name="description" content="[^"]*"/, `<meta name="description" content="${category.seo_description}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('获取文章详情用于SEO渲染失败: ' + String(e))
|
||||||
|
}
|
||||||
|
|
||||||
response.writeHead(httpOk, { 'Content-type': 'text/html;charset=utf-8' })
|
response.writeHead(httpOk, { 'Content-type': 'text/html;charset=utf-8' })
|
||||||
response.end(html)
|
response.end(finalHtml)
|
||||||
logger.info(`${request.method} ${httpOk} ${request.url}`)
|
logger.info(`${request.method} ${httpOk} ${request.url}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,13 @@ export default {
|
||||||
return await get(appConfig.baseURL, "/user", data);
|
return await get(appConfig.baseURL, "/user", data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 6. 退出登录
|
||||||
|
*/
|
||||||
|
async logout(data = {}) {
|
||||||
|
return await postJson(appConfig.baseURL, "/user/logout", data);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取商品列表
|
* 获取商品列表
|
||||||
* @param {*} data
|
* @param {*} data
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// 创建专属实例,baseURL 指向刚才配置的代理前缀
|
||||||
|
const wechatHttp = axios.create({
|
||||||
|
baseURL: '/wechat-api',
|
||||||
|
timeout: 15000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器 (按需配置)
|
||||||
|
wechatHttp.interceptors.request.use(config => {
|
||||||
|
// 微信接口如果需要特定的 token,可以在这里统一携带
|
||||||
|
// 例如:config.headers['X-Token'] = '...'
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
wechatHttp.interceptors.response.use(
|
||||||
|
res => {
|
||||||
|
// 假设 Apifox 显示返回直接是数据数组,不包装在 data 字段内,
|
||||||
|
// 则 axios 会自动将其放在 res.data 下。
|
||||||
|
// 如果实际外层还有 code, msg 字段,请在这里根据实际情况判断拦截。
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
if (!err.config?._silent) {
|
||||||
|
const msg = err.response?.data?.message || err.message || '微信接口请求失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
}
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 导出具体的接口调用方法
|
||||||
|
export const wechatApi = {
|
||||||
|
// 获取首页 banner 图
|
||||||
|
getBanners: () => wechatHttp.get('/api/public/banners'),
|
||||||
|
// 获取文章分类
|
||||||
|
getArticlesCategories: () => wechatHttp.get('/api/articles/categories'),
|
||||||
|
// 获取文章列表
|
||||||
|
getArticlesList: (params) => wechatHttp.get('/api/articles', { params }),
|
||||||
|
// 获取文章详情
|
||||||
|
getArticleDetail: (id) => wechatHttp.get(`/api/articles/${id}`),
|
||||||
|
// 获取相关推荐文章
|
||||||
|
getRelatedArticles: (id) => wechatHttp.get(`/api/articles/${id}/related`),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default wechatApi
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="type === 'large'" class="feature-card" @click="$emit('click')">
|
<div v-if="type === 'large'" class="feature-card" @click="$emit('itemClick')">
|
||||||
<div class="row-top">
|
<div class="row-top">
|
||||||
<img class="feature-img" :src="icon" />
|
<img class="feature-img" :src="icon" />
|
||||||
<div class="text-col">
|
<div class="text-col">
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="tool-card" @click="$emit('click')">
|
<div v-else class="tool-card" @click="$emit('itemClick')">
|
||||||
<img class="tool-img" :src="icon" />
|
<img class="tool-img" :src="icon" />
|
||||||
<div class="tool-text">
|
<div class="tool-text">
|
||||||
<span class="tool-title"> {{ title }} </span>
|
<span class="tool-title"> {{ title }} </span>
|
||||||
|
|
@ -40,7 +40,7 @@ defineProps({
|
||||||
default: 'small'
|
default: 'small'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
defineEmits(['click'])
|
defineEmits(['itemClick'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,9 @@
|
||||||
<div class="app-download-wrapper">
|
<div class="app-download-wrapper">
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<div class="scan-title">扫码下载装样大师手机版</div>
|
<div class="scan-title">扫码下载装样大师手机版</div>
|
||||||
<img class="qrcode" :src="appCodeImg" alt="">
|
<div class="qrcode" v-loading="appCodeLoading">
|
||||||
|
<img class="qrcode" v-if="appCodeImg" :src="appCodeImg" alt="">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
@ -102,9 +104,12 @@ const showAppCode = ref(false)
|
||||||
const appCodeImg = ref('')
|
const appCodeImg = ref('')
|
||||||
|
|
||||||
|
|
||||||
|
const appCodeLoading = ref(false)
|
||||||
|
|
||||||
async function openAppCodeDialog() {
|
async function openAppCodeDialog() {
|
||||||
showAppCode.value = true
|
showAppCode.value = true
|
||||||
if (!appCodeImg.value) {
|
if (!appCodeImg.value) {
|
||||||
|
appCodeLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await authService.getDownloadCode()
|
const res = await authService.getDownloadCode()
|
||||||
console.log('获取APP二维码响应结果:', res)
|
console.log('获取APP二维码响应结果:', res)
|
||||||
|
|
@ -112,6 +117,8 @@ async function openAppCodeDialog() {
|
||||||
appCodeImg.value = data.android_qrcode
|
appCodeImg.value = data.android_qrcode
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e.message)
|
console.log(e.message)
|
||||||
|
} finally {
|
||||||
|
appCodeLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,11 +162,11 @@ const navList = [
|
||||||
key: 'entertainment',
|
key: 'entertainment',
|
||||||
children: [
|
children: [
|
||||||
{ name: '工资单', key: 'gongzidan', path: '/gongzidan', icon: '/src/public/header/zhengjianmoni.png' },
|
{ name: '工资单', key: 'gongzidan', path: '/gongzidan', icon: '/src/public/header/zhengjianmoni.png' },
|
||||||
{ name: '飞机票', key: 'flight', path: '/flight', icon: '/src/public/header/feijipiao.png' },
|
{ name: '飞机票', key: 'flight', path: '', icon: '/src/public/header/feijipiao.png' },
|
||||||
{ name: '火车票', key: 'train', path: '/train', icon: '/src/public/header/huochepiao.png' },
|
{ name: '火车票', key: 'train', path: '', icon: '/src/public/header/huochepiao.png' },
|
||||||
{ name: '购物详情', key: 'shopping', path: '/shopping', icon: '/src/public/header/gouwuxiangqing.png' },
|
{ name: '购物详情', key: 'shopping', path: '', icon: '/src/public/header/gouwuxiangqing.png' },
|
||||||
{ name: '情侣证', key: 'couple', path: '/couple', icon: '/src/public/header/qinglvzheng.png' },
|
{ name: '情侣证', key: 'couple', path: '', icon: '/src/public/header/qinglvzheng.png' },
|
||||||
{ name: '视频聊天', key: 'video', path: '/video', icon: '/src/public/header/shipinliaotian.png' }
|
{ name: '视频聊天', key: 'video', path: '', icon: '/src/public/header/shipinliaotian.png' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ name: '作品库', key: 'works', path: '/works' },
|
{ name: '作品库', key: 'works', path: '/works' },
|
||||||
|
|
@ -178,6 +185,10 @@ function handleNavClick(item) {
|
||||||
if (item.path) {
|
if (item.path) {
|
||||||
router.push(item.path)
|
router.push(item.path)
|
||||||
}
|
}
|
||||||
|
if (!item.path) {
|
||||||
|
ElMessage.warning("该功能暂未上线")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isItemActive(item) {
|
function isItemActive(item) {
|
||||||
|
|
@ -191,7 +202,7 @@ function isItemActive(item) {
|
||||||
|
|
||||||
async function handleCommand(cmd) {
|
async function handleCommand(cmd) {
|
||||||
if (cmd === 'logout') {
|
if (cmd === 'logout') {
|
||||||
try { await api.auth.logout() } catch { }
|
try { await authService.logout() } catch { }
|
||||||
userStore.reset()
|
userStore.reset()
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
localStorage.removeItem('user_info')
|
localStorage.removeItem('user_info')
|
||||||
|
|
@ -223,7 +234,7 @@ function openLoginDialog() {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 2000px;
|
//max-width: 2000px;
|
||||||
min-width: 1200px;
|
min-width: 1200px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 1px 8px rgba(0, 0, 0, .08);
|
box-shadow: 0 1px 8px rgba(0, 0, 0, .08);
|
||||||
|
|
|
||||||
|
|
@ -190,8 +190,12 @@ defineExpose({ previewAreaRef })
|
||||||
.panel-scroll {
|
.panel-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 580px;
|
max-height: 100%;
|
||||||
padding: 18px 68px 0;
|
padding: 18px 68px 0;
|
||||||
|
|
||||||
|
.setting-list {
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ module.exports = {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path
|
rewrite: (path) => path
|
||||||
},
|
},
|
||||||
'/static': {
|
'/wechat-api': {
|
||||||
target: 'http://wechat.wenyitu.com',
|
target: 'http://wechat.wenyitu.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path
|
rewrite: (path) => path.replace(/^\/wechat-api/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -182,6 +182,14 @@ function statusLabel(s) {
|
||||||
return { paid: '已支付', pending: '待支付', expired: '已过期', refunded: '已退款' }[s] || s
|
return { paid: '已支付', pending: '待支付', expired: '已过期', refunded: '已退款' }[s] || s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
authService.logout().catch(() => {})
|
||||||
|
userStore.reset()
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user_info')
|
||||||
|
ElMessage.success('已退出登录')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="article-detail-wrapper">
|
<div class="article-detail-wrapper">
|
||||||
<div class="main-container">
|
<div class="main-container" v-loading="loading" element-loading-text="正在加载..."
|
||||||
|
element-loading-background="rgba(248, 249, 251, 0.8)">
|
||||||
<!-- 左侧核心内容区 -->
|
<!-- 左侧核心内容区 -->
|
||||||
<div class="article-left-content">
|
<div class="article-left-content">
|
||||||
<!-- 顶部返回按钮 -->
|
<!-- 顶部返回按钮 -->
|
||||||
|
|
@ -27,37 +27,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文章导读区 -->
|
<!-- 文章导读区 -->
|
||||||
<div class="article-intro-box">
|
<div v-if="currentArticle.intro" class="article-intro-box">
|
||||||
<div class="intro-header">
|
<div class="intro-header">
|
||||||
<img class="star-icon" src="@/public/article/start.png" />
|
<img class="star-icon" src="@/public/article/start.png" />
|
||||||
<span>文章导读</span>
|
<span>文章导读</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="intro-content">{{ currentArticle.intro || currentArticle.description }}</p>
|
<p class="intro-content">{{ currentArticle.intro }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 正文区块 -->
|
<!-- 正文区块 -->
|
||||||
<div class="article-body-content">
|
<div class="article-body-content markdown-body" v-html="parsedContent">
|
||||||
<template v-if="currentArticle.sections && currentArticle.sections.length">
|
|
||||||
<div v-for="(sec, idx) in currentArticle.sections" :key="idx" class="section-block">
|
|
||||||
<h3 class="section-title">{{ sec.heading }}</h3>
|
|
||||||
<p class="section-text">{{ sec.content }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="section-block">
|
|
||||||
<h3 class="section-title">一、功能介绍</h3>
|
|
||||||
<p class="section-text">
|
|
||||||
我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 文章插图 -->
|
|
||||||
<div class="article-image-box">
|
|
||||||
<img v-if="currentArticle.image" :src="currentArticle.image" alt="文章插图"
|
|
||||||
class="article-img" />
|
|
||||||
<img v-else src="https://picsum.photos/800/320?random=1" alt="演示图" class="article-img" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 上一篇 / 下一篇 导航卡片 -->
|
<!-- 上一篇 / 下一篇 导航卡片 -->
|
||||||
|
|
@ -120,130 +99,156 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部警告组件 -->
|
<!-- 底部警告组件 -->
|
||||||
<WarningFooter />
|
<WarningFooter />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import WarningFooter from '@/components/common/WarningFooter.vue'
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
|
import { wechatApi } from '@/api/wechat'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
// 当前文章 ID
|
// 当前文章 ID
|
||||||
const currentId = ref(Number(route.query.id) || 1)
|
const currentId = ref(route.query.id || '')
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
watch(() => route.query.id, (newId) => {
|
// 数据状态
|
||||||
|
const currentArticle = ref({})
|
||||||
|
const recommendList = ref([])
|
||||||
|
const prevArticle = ref(null)
|
||||||
|
const nextArticle = ref(null)
|
||||||
|
|
||||||
|
// 计算 Markdown HTML
|
||||||
|
const parsedContent = computed(() => {
|
||||||
|
if (!currentArticle.value.content_md) return ''
|
||||||
|
return marked.parse(currentArticle.value.content_md)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取文章详情
|
||||||
|
const fetchArticleDetail = async (id) => {
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
let res = await wechatApi.getArticleDetail(id)
|
||||||
|
res = typeof res === 'string' ? JSON.parse(res) : res
|
||||||
|
currentArticle.value = {
|
||||||
|
...res,
|
||||||
|
title: res.title,
|
||||||
|
date: res.published_at ? res.published_at.split('T')[0] : '',
|
||||||
|
views: res.views_count,
|
||||||
|
intro: res.summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态修改页面 SEO 信息
|
||||||
|
if (res.title) {
|
||||||
|
document.title = `${res.title} - 装样大师`
|
||||||
|
}
|
||||||
|
if (res.seo_keywords) {
|
||||||
|
const metaKeywords = document.querySelector('meta[name="keywords"]')
|
||||||
|
if (metaKeywords) metaKeywords.setAttribute('content', res.seo_keywords)
|
||||||
|
}
|
||||||
|
if (res.seo_description) {
|
||||||
|
const metaDesc = document.querySelector('meta[name="description"]')
|
||||||
|
if (metaDesc) metaDesc.setAttribute('content', res.seo_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文章详情失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取相关推荐列表
|
||||||
|
const fetchRecommendList = async (id) => {
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
let res = await wechatApi.getRelatedArticles(id)
|
||||||
|
res = typeof res === 'string' ? JSON.parse(res) : res
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
recommendList.value = res.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
date: item.published_at ? item.published_at.split('T')[0] : '',
|
||||||
|
views: item.views_count
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取相关推荐失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取上下篇文章
|
||||||
|
const fetchPrevNextArticles = async (id) => {
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
let res = await wechatApi.getArticlesList({ page: 1, per_page: 500 })
|
||||||
|
res = typeof res === 'string' ? JSON.parse(res) : res
|
||||||
|
if (res && res.items) {
|
||||||
|
let list = res.items
|
||||||
|
|
||||||
|
// 先按照 is_hot 优先,再按时间倒序
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const aHot = !!a.is_hot
|
||||||
|
const bHot = !!b.is_hot
|
||||||
|
if (aHot && !bHot) return -1
|
||||||
|
if (!aHot && bHot) return 1
|
||||||
|
const aTime = a.published_at ? new Date(a.published_at).getTime() : 0
|
||||||
|
const bTime = b.published_at ? new Date(b.published_at).getTime() : 0
|
||||||
|
return bTime - aTime
|
||||||
|
})
|
||||||
|
|
||||||
|
const index = list.findIndex(item => item.id == id)
|
||||||
|
if (index !== -1) {
|
||||||
|
// 根据时间倒序排列:上一篇更近,下一篇更远
|
||||||
|
prevArticle.value = index > 0 ? list[index - 1] : null
|
||||||
|
nextArticle.value = index < list.length - 1 ? list[index + 1] : null
|
||||||
|
} else {
|
||||||
|
prevArticle.value = null
|
||||||
|
nextArticle.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取上下篇文章失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (currentId.value) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
fetchArticleDetail(currentId.value),
|
||||||
|
fetchRecommendList(currentId.value),
|
||||||
|
fetchPrevNextArticles(currentId.value)
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => route.query.id, async (newId) => {
|
||||||
if (newId) {
|
if (newId) {
|
||||||
currentId.value = Number(newId)
|
currentId.value = newId
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
fetchArticleDetail(newId),
|
||||||
|
fetchRecommendList(newId),
|
||||||
|
fetchPrevNextArticles(newId)
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock 完整文章数据库
|
|
||||||
const articleDatabase = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: '微商模拟神器-节假日活动必备玩法',
|
|
||||||
date: '2026-7-20',
|
|
||||||
views: '11.5K',
|
|
||||||
intro: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: '一、功能介绍',
|
|
||||||
content: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
image: 'https://picsum.photos/800/320?random=10'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '微商模拟神器-节假日活动必备玩法超超超推荐',
|
|
||||||
date: '2026-7-20',
|
|
||||||
views: '11.5K',
|
|
||||||
intro: '全网首创的高逼真恶搞模拟工具,支持自定义头像、对话内容、账单金额与交易状态,各种场景真实不穿帮。',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: '一、核心亮点',
|
|
||||||
content: '一键生成高清对话截图,像素级细节还原,支持无限修改,帮助你轻松制作搞笑段子与社交互动图。'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
image: 'https://picsum.photos/800/320?random=11'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: '微商模拟神器-节假日活动必备玩法超超超推荐HHHHH...',
|
|
||||||
date: '2026-7-20',
|
|
||||||
views: '11.5K',
|
|
||||||
intro: '无论是发朋友圈互动还是朋友间轻松恶搞,装样大师都能提供全套的模拟支持。',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: '一、快速上手',
|
|
||||||
content: '无需复杂的操作步骤,选择模板填入文字即可一键导出无水印图片。'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
image: 'https://picsum.photos/800/320?random=12'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: '微信神器转账支付怎么制作?装样大师新手教程',
|
|
||||||
date: '2026-7-20',
|
|
||||||
views: '11.5K',
|
|
||||||
intro: '新手必看!如何三步快速生成高清逼真的转账与零钱余额截图。',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: '一、教程步骤',
|
|
||||||
content: '打开微信模拟功能,输入期望金额与收付款人信息,点击保存按钮导出图片。'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
image: 'https://picsum.photos/800/320?random=13'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: '工资单与机票模拟生成器全面上线说明',
|
|
||||||
date: '2026-7-20',
|
|
||||||
views: '11.5K',
|
|
||||||
intro: '全新升级娱乐模拟玩法,支持模拟机票、高铁票及个人工资单记录。',
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
heading: '一、更新公告',
|
|
||||||
content: '优化底纹防伪效果,提升了图片生成的画质与加载速度。'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
image: 'https://picsum.photos/800/320?random=14'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 匹配当前 ID 对应的文章
|
|
||||||
const currentArticle = computed(() => {
|
|
||||||
return articleDatabase.value.find(item => item.id === currentId.value) || articleDatabase.value[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算上一篇与下一篇
|
|
||||||
const currentIndex = computed(() => {
|
|
||||||
return articleDatabase.value.findIndex(item => item.id === currentArticle.value.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
const prevArticle = computed(() => {
|
|
||||||
if (currentIndex.value > 0) {
|
|
||||||
return articleDatabase.value[currentIndex.value - 1]
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const nextArticle = computed(() => {
|
|
||||||
if (currentIndex.value < articleDatabase.value.length - 1) {
|
|
||||||
return articleDatabase.value[currentIndex.value + 1]
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
// 相关推荐列表(包含 5 条数据)
|
|
||||||
const recommendList = computed(() => articleDatabase.value)
|
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
router.push('/article')
|
router.push('/article')
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +299,7 @@ function handleSwitchArticle(articleItem) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1340px) {
|
||||||
.recommend-sidebar {
|
.recommend-sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
/* 屏幕过窄时隐藏侧边栏避免挡住主卡片 */
|
/* 屏幕过窄时隐藏侧边栏避免挡住主卡片 */
|
||||||
|
|
@ -400,36 +405,33 @@ function handleSwitchArticle(articleItem) {
|
||||||
|
|
||||||
/* 正文 */
|
/* 正文 */
|
||||||
.article-body-content {
|
.article-body-content {
|
||||||
.section-block {
|
margin-top: 32px;
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 26px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1A1A1A;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-text {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #1A1A1A;
|
color: #1A1A1A;
|
||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-image-box {
|
.article-body-content :deep(img) {
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
height: 320px;
|
border-radius: 8px;
|
||||||
border-radius: 4px;
|
margin: 16px 0;
|
||||||
overflow: hidden;
|
}
|
||||||
margin-bottom: 30px;
|
|
||||||
|
|
||||||
.article-img {
|
.article-body-content :deep(h1),
|
||||||
width: 100%;
|
.article-body-content :deep(h2),
|
||||||
height: 100%;
|
.article-body-content :deep(h3) {
|
||||||
object-fit: cover;
|
color: #1A1A1A;
|
||||||
}
|
margin-top: 24px;
|
||||||
}
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-body-content :deep(p) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-body-content :deep(a) {
|
||||||
|
color: #8B70FF;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 上一篇/下一篇 导航卡片 */
|
/* 上一篇/下一篇 导航卡片 */
|
||||||
|
|
@ -650,4 +652,8 @@ function handleSwitchArticle(articleItem) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="article-page-wrapper">
|
<div class="article-page-wrapper">
|
||||||
<div class="article-list-container">
|
<div class="main-container">
|
||||||
|
<div class="article-list-container" v-loading="loading" style="min-height: 400px;">
|
||||||
<div v-for="item in articleList" :key="item.id" class="article-card" @click="handleCardClick(item)">
|
<div v-for="item in articleList" :key="item.id" class="article-card" @click="handleCardClick(item)">
|
||||||
<!-- 卡片头部信息 -->
|
<!-- 卡片头部信息 -->
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h2 class="article-title" :title="item.title">{{ item.title }}</h2>
|
<h1 class="article-title" :title="item.title">{{ item.title }}</h1>
|
||||||
<div v-if="item.isHot" class="hot-badge">
|
<div v-if="item.isHot" class="hot-badge">
|
||||||
<img class="hot-icon" src="@/public/article/hot.png" alt="">
|
<img class="hot-icon" src="@/public/article/hot.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,8 +27,8 @@
|
||||||
<!-- 卡片主体内容 -->
|
<!-- 卡片主体内容 -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="cover-thumb">
|
<div class="cover-thumb">
|
||||||
<img v-if="item.cover" :src="item.cover" :alt="item.title" class="cover-img" />
|
<img :src="item.cover || '/src/public/article/empty.png'" :alt="item.title"
|
||||||
<div v-else class="cover-placeholder"></div>
|
class="cover-img" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-right">
|
<div class="content-right">
|
||||||
|
|
@ -38,6 +39,35 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 暂无数据提示 -->
|
||||||
|
<el-empty v-if="!loading && articleList.length === 0" description="暂无文章" image-size="200"
|
||||||
|
image="/src/public/profile/empty.png" />
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<div class="pagination-container" v-if="total > queryParams.per_page">
|
||||||
|
<el-pagination class="custom-pagination" layout="total, prev, pager, next, jumper" prev-text="上一页"
|
||||||
|
next-text="下一页" :total="total" :page-size="queryParams.per_page"
|
||||||
|
v-model:current-page="queryParams.page" @current-change="handlePageChange"
|
||||||
|
hide-on-single-page />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧分类侧边栏 -->
|
||||||
|
<div class="category-sidebar">
|
||||||
|
<div class="sidebar-card">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<img src="@/public/article/tuijian.png" class="category-icon" />
|
||||||
|
<span class="sidebar-title">类型</span>
|
||||||
|
</div>
|
||||||
|
<div class="category-list">
|
||||||
|
<div v-for="cat in categoryList" :key="cat.id" class="category-item"
|
||||||
|
:class="{ active: currentCategory === cat.id }" @click="handleCategoryClick(cat)">
|
||||||
|
{{ cat.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部警告说明组件 -->
|
<!-- 底部警告说明组件 -->
|
||||||
|
|
@ -46,51 +76,164 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import WarningFooter from '@/components/common/WarningFooter.vue'
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
|
import { wechatApi } from '@/api/wechat'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
// 依照效果图高保真模拟数据(支持接口数据列表直接替换)
|
// 状态与数据
|
||||||
const articleList = ref([
|
const loading = ref(false)
|
||||||
{
|
const categoryList = ref([{ id: '', name: '全部文章' }])
|
||||||
id: 1,
|
const currentCategory = ref('')
|
||||||
title: '微商模拟神器',
|
const articleList = ref([])
|
||||||
isHot: true,
|
|
||||||
date: '2026-7-20',
|
// 分页与参数
|
||||||
views: '11.5K',
|
const queryParams = ref({
|
||||||
cover: '',
|
page: 1,
|
||||||
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
|
per_page: 10,
|
||||||
},
|
category_id: '',
|
||||||
{
|
keyword: ''
|
||||||
id: 2,
|
})
|
||||||
title: '微商模拟神器哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈好...',
|
const total = ref(0)
|
||||||
isHot: true,
|
|
||||||
date: '2026-7-20',
|
// 获取当前分类的名称
|
||||||
views: '11.5K',
|
const currentCategoryName = computed(() => {
|
||||||
cover: '',
|
const cat = categoryList.value.find(c => c.id === currentCategory.value)
|
||||||
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈...'
|
return cat ? cat.name : '全部文章'
|
||||||
},
|
})
|
||||||
{
|
|
||||||
id: 1,
|
// 监听分类名称变化,动态更新页面标题
|
||||||
title: '微商模拟神器',
|
watch(currentCategoryName, (newName) => {
|
||||||
isHot: true,
|
if (newName !== '全部文章') {
|
||||||
date: '2026-7-20',
|
document.title = `${newName}-装样大师`
|
||||||
views: '11.5K',
|
} else {
|
||||||
cover: '',
|
document.title = `装样大师`
|
||||||
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '微商模拟神器哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈好哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈',
|
|
||||||
isHot: true,
|
|
||||||
date: '2026-7-20',
|
|
||||||
views: '11.5K',
|
|
||||||
cover: '',
|
|
||||||
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈...'
|
|
||||||
}
|
}
|
||||||
])
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 获取分类数据
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
try {
|
||||||
|
let res = await wechatApi.getArticlesCategories()
|
||||||
|
res = typeof res === 'string' ? JSON.parse(res) : res
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
categoryList.value = [
|
||||||
|
{ id: '', name: '全部文章' },
|
||||||
|
...res
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取分类失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文章列表数据
|
||||||
|
const fetchArticles = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
let res = await wechatApi.getArticlesList(queryParams.value)
|
||||||
|
res = typeof res === 'string' ? JSON.parse(res) : res
|
||||||
|
|
||||||
|
if (res && res.items) {
|
||||||
|
// 设置分类 SEO 信息
|
||||||
|
if (currentCategory.value != '' && res.items.length > 0 && res.items[0].category) {
|
||||||
|
const categoryData = res.items[0].category
|
||||||
|
const metaKeywords = document.querySelector('meta[name="keywords"]')
|
||||||
|
if (metaKeywords && categoryData.seo_keywords) {
|
||||||
|
metaKeywords.setAttribute('content', categoryData.seo_keywords)
|
||||||
|
}
|
||||||
|
const metaDesc = document.querySelector('meta[name="description"]')
|
||||||
|
if (metaDesc && categoryData.seo_description) {
|
||||||
|
metaDesc.setAttribute('content', categoryData.seo_description)
|
||||||
|
}
|
||||||
|
} else if (currentCategory.value === '') {
|
||||||
|
// 恢复全部文章的基础 SEO 信息
|
||||||
|
const metaKeywords = document.querySelector('meta[name="keywords"]')
|
||||||
|
if (metaKeywords) metaKeywords.setAttribute('content', '装样大师, 模拟器')
|
||||||
|
const metaDesc = document.querySelector('meta[name="description"]')
|
||||||
|
if (metaDesc) metaDesc.setAttribute('content', '装样大师是一款高保真的模拟工具')
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappedList = res.items.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
isHot: item.is_hot,
|
||||||
|
date: item.published_at ? item.published_at.split('T')[0] : '',
|
||||||
|
timestamp: item.published_at ? new Date(item.published_at).getTime() : 0,
|
||||||
|
views: item.views_count,
|
||||||
|
cover: item.cover_url,
|
||||||
|
description: item.summary
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 双重排序:优先判断 is_hot,其次按照时间倒序
|
||||||
|
mappedList.sort((a, b) => {
|
||||||
|
const aHot = !!a.isHot
|
||||||
|
const bHot = !!b.isHot
|
||||||
|
if (aHot && !bHot) return -1
|
||||||
|
if (!aHot && bHot) return 1
|
||||||
|
return b.timestamp - a.timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
articleList.value = mappedList
|
||||||
|
total.value = res.total
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取文章失败:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一更新 URL 逻辑
|
||||||
|
function updateQueryToUrl() {
|
||||||
|
const query = {}
|
||||||
|
if (queryParams.value.category_id) query.category_id = queryParams.value.category_id
|
||||||
|
if (queryParams.value.keyword) query.keyword = queryParams.value.keyword
|
||||||
|
if (queryParams.value.page > 1) query.page = queryParams.value.page
|
||||||
|
|
||||||
|
router.push({ path: route.path, query })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻页事件
|
||||||
|
function handlePageChange(page) {
|
||||||
|
queryParams.value.page = page
|
||||||
|
updateQueryToUrl()
|
||||||
|
// 翻页后平滑滚动回顶部
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击分类加载对应文章
|
||||||
|
function handleCategoryClick(cat) {
|
||||||
|
if (currentCategory.value === cat.id) return // 避免重复点击
|
||||||
|
|
||||||
|
currentCategory.value = cat.id
|
||||||
|
queryParams.value.category_id = cat.id
|
||||||
|
queryParams.value.page = 1
|
||||||
|
updateQueryToUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由参数变化,同步到状态并拉取数据
|
||||||
|
watch(() => route.query, (newQuery) => {
|
||||||
|
queryParams.value.category_id = newQuery.category_id ? Number(newQuery.category_id) : ''
|
||||||
|
queryParams.value.keyword = newQuery.keyword || ''
|
||||||
|
queryParams.value.page = newQuery.page ? Number(newQuery.page) : 1
|
||||||
|
currentCategory.value = queryParams.value.category_id
|
||||||
|
|
||||||
|
fetchArticles()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 恢复列表页基础 SEO 信息
|
||||||
|
const metaKeywords = document.querySelector('meta[name="keywords"]')
|
||||||
|
if (metaKeywords) metaKeywords.setAttribute('content', '装样大师, 模拟器')
|
||||||
|
const metaDesc = document.querySelector('meta[name="description"]')
|
||||||
|
if (metaDesc) metaDesc.setAttribute('content', '装样大师是一款高保真的模拟工具')
|
||||||
|
|
||||||
|
fetchCategories()
|
||||||
|
})
|
||||||
|
|
||||||
function handleDetail(item) {
|
function handleDetail(item) {
|
||||||
router.push({
|
router.push({
|
||||||
|
|
@ -115,16 +258,34 @@ function handleCardClick(item) {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-list-container {
|
.main-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 820px;
|
max-width: 1140px;
|
||||||
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.article-list-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
height: 74px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.article-card {
|
.article-card {
|
||||||
width: 800px;
|
width: 100%;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 34px;
|
padding: 34px;
|
||||||
|
|
@ -307,4 +468,161 @@ function handleCardClick(item) {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.main-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏分类样式 */
|
||||||
|
.category-sidebar {
|
||||||
|
width: 230px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 152px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item {
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333333;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
// color: #8B70FF;
|
||||||
|
background-color: #F8F9FB;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #8B70FF;
|
||||||
|
background-color: #EFEEFF;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义分页组件样式 */
|
||||||
|
:deep(.custom-pagination) {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.el-pagination__total {
|
||||||
|
margin-right: 20px;
|
||||||
|
color: #3D3D3D;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-prev,
|
||||||
|
.btn-next {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: 1px solid #E9E9E9;
|
||||||
|
background-color: #FAFAFA !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: #3D3D3D;
|
||||||
|
font-weight: normal;
|
||||||
|
min-width: 60px;
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 37px;
|
||||||
|
line-height: 37px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-prev:hover,
|
||||||
|
.btn-next:hover {
|
||||||
|
color: #8B70FF;
|
||||||
|
border-color: #8B70FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pager li {
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: #3D3D3D;
|
||||||
|
font-weight: normal;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 4px;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.is-active) {
|
||||||
|
color: #8B70FF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination__jump {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-left: 12px;
|
||||||
|
color: #3D3D3D;
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
height: 37px;
|
||||||
|
line-height: 37px;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #8B70FF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: #8B70FF !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="div">
|
<div class="div">
|
||||||
<div class="flexcontainer">
|
<div class="flexcontainer">
|
||||||
<div class="banner-box">
|
<div class="banner-box" v-loading="loading" style="min-height: 450px;">
|
||||||
<img class="icon left-icon" src="@/public/index/left.png" alt="">
|
<!-- 数据加载完毕,且只有一张图时直接显示 -->
|
||||||
<img class="icon right-icon" src="@/public/index/right.png" alt="">
|
<template v-if="!loading && bannerList.length === 1">
|
||||||
<img class="banner" src="@/public/index/banner-1.jpg" alt="" mode="widthFix">
|
<img class="banner" :src="bannerList[0].image_url" alt=""
|
||||||
|
@click="handleBannerClick(bannerList[0].link_url)"
|
||||||
|
style="cursor: pointer; width: 100%; height: auto; display: block;">
|
||||||
|
</template>
|
||||||
|
<!-- 数据加载完毕,且大于一张图时使用轮播 -->
|
||||||
|
<template v-else-if="!loading && bannerList.length > 1">
|
||||||
|
<el-carousel :interval="5000" arrow="hover" class="banner-carousel" :height="carouselHeight">
|
||||||
|
<el-carousel-item v-for="item in bannerList" :key="item.id">
|
||||||
|
<img class="banner" :src="item.image_url" alt="" @click="handleBannerClick(item.link_url)"
|
||||||
|
@load="handleImageLoad"
|
||||||
|
style="cursor: pointer; width: 100%; height: auto; display: block;">
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="rectangle_33_1">
|
<div class="rectangle_33_1">
|
||||||
<span class="div_3">
|
<span class="div_3">
|
||||||
|
|
@ -31,7 +44,7 @@
|
||||||
<span class="text_26"> 万物皆可模拟 , 认真你就输了,毕业证、机票、工资单,想要啥有啥,恶搞到底,而且像到亲妈都信 </span>
|
<span class="text_26"> 万物皆可模拟 , 认真你就输了,毕业证、机票、工资单,想要啥有啥,恶搞到底,而且像到亲妈都信 </span>
|
||||||
<div class="tools-grid">
|
<div class="tools-grid">
|
||||||
<ToolCard v-for="(item, index) in toolList" :key="index" :icon="item.icon" :title="item.title"
|
<ToolCard v-for="(item, index) in toolList" :key="index" :icon="item.icon" :title="item.title"
|
||||||
:desc="item.desc" />
|
:desc="item.desc" @itemClick="handleToolCardClick(item.path)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -55,19 +68,81 @@
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, resolveDirective } from 'vue'
|
||||||
import AppFooter from '@/components/common/AppFooter.vue'
|
import AppFooter from '@/components/common/AppFooter.vue'
|
||||||
import ToolCard from '@/components/ToolCard.vue'
|
import ToolCard from '@/components/ToolCard.vue'
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { wechatApi } from '@/api/wechat'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const bannerList = ref([])
|
||||||
|
const carouselHeight = ref('450px')
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 动态更新轮播图高度
|
||||||
|
const updateCarouselHeight = () => {
|
||||||
|
const banners = document.querySelectorAll('.banner-carousel .banner')
|
||||||
|
if (banners.length > 0 && banners[0].offsetHeight) {
|
||||||
|
carouselHeight.value = banners[0].offsetHeight + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载完成后初始化高度
|
||||||
|
const handleImageLoad = (e) => {
|
||||||
|
if (e.target && e.target.offsetHeight) {
|
||||||
|
carouselHeight.value = e.target.offsetHeight + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 Banner 列表
|
||||||
|
const fetchBanners = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await wechatApi.getBanners()
|
||||||
|
const data = res ? JSON.parse(res) : []
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
console.log("banner-data", data)
|
||||||
|
// 根据返回数据的 sort_order 进行排序
|
||||||
|
bannerList.value = data.sort((a, b) => a.sort_order - b.sort_order)
|
||||||
|
console.log("banner列表", bannerList.value)
|
||||||
|
}
|
||||||
|
console.log("bannerList.value", bannerList.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取Banner失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击 Banner 跳转
|
||||||
|
const handleBannerClick = (link) => {
|
||||||
|
if (!link) return
|
||||||
|
if (link.startsWith('http')) {
|
||||||
|
window.open(link, '_blank')
|
||||||
|
} else {
|
||||||
|
router.push(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchBanners()
|
||||||
|
window.addEventListener('resize', updateCarouselHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateCarouselHeight)
|
||||||
|
})
|
||||||
|
|
||||||
const toolList = [
|
const toolList = [
|
||||||
{ icon: "/src/public/index/gongzidan.png", title: "工资单", desc: "月入五万截图保真" },
|
{ icon: "/src/public/index/gongzidan.png", title: "工资单", desc: "月入五万截图保真", path: "/gongzidan" },
|
||||||
{ icon: "/src/public/index/huochepiao.png", title: "火车票", desc: "说走就走的「假旅行」" },
|
{ icon: "/src/public/index/huochepiao.png", title: "火车票", desc: "说走就走的「假旅行」", path: "" },
|
||||||
{ icon: "/src/public/index/feijipiao.png", title: "飞机票", desc: "票比真的还像" },
|
{ icon: "/src/public/index/feijipiao.png", title: "飞机票", desc: "票比真的还像", path: "" },
|
||||||
{ icon: "/src/public/index/gouwumoni.png", title: "购物模拟", desc: "假装清空购物车" },
|
{ icon: "/src/public/index/gouwumoni.png", title: "购物模拟", desc: "假装清空购物车", path: "" },
|
||||||
{ icon: "/src/public/index/zhengjian.png", title: "证件", desc: "防伪底纹都还原" },
|
{ icon: "/src/public/index/zhengjian.png", title: "证件", desc: "防伪底纹都还原", path: "" },
|
||||||
{ icon: "/src/public/index/liaotianmoni.png", title: "视频群聊", desc: "视频通话中…" }
|
{ icon: "/src/public/index/liaotianmoni.png", title: "视频群聊", desc: "视频通话中…", path: "" }
|
||||||
]
|
]
|
||||||
|
|
||||||
const serviceList = [
|
const serviceList = [
|
||||||
|
|
@ -90,6 +165,10 @@ const serviceList = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleToolCardClick = (path) => {
|
const handleToolCardClick = (path) => {
|
||||||
|
if (!path) {
|
||||||
|
ElMessage.warning("该功能暂未上线")
|
||||||
|
return
|
||||||
|
}
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -540,4 +619,24 @@ const handleToolCardClick = (path) => {
|
||||||
margin-top: 45px;
|
margin-top: 45px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner-carousel {
|
||||||
|
:deep(.el-carousel__arrow--left) {
|
||||||
|
left: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__arrow--right) {
|
||||||
|
right: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__arrow) {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
// display: block !important;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 24px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,9 @@ const randomGenerate = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveDraft = async () => {
|
const saveDraft = async () => {
|
||||||
if (!localStorage.getItem('token')) {
|
if (!userStore.isLoggedIn) {
|
||||||
ElMessage.warning('请先登录')
|
ElMessage.warning('请先登录')
|
||||||
|
router.replace({ query: { ...route.query, showLogin: 1 } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -321,7 +322,11 @@ const saveDraft = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadPaper = async () => {
|
const downloadPaper = async () => {
|
||||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
if (!userStore.isLoggedIn) {
|
||||||
|
ElMessage.warning('请先登录后再下载')
|
||||||
|
router.replace({ query: { ...route.query, showLogin: 1 } })
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!userStore.isVip) {
|
if (!userStore.isVip) {
|
||||||
showPayment?.()
|
showPayment?.()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, inject, onMounted, watch, computed } from 'vue'
|
import { ref, reactive, inject, onMounted, watch, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { FolderAdd, Download } from '@element-plus/icons-vue'
|
import { FolderAdd, Download } from '@element-plus/icons-vue'
|
||||||
import BalancePreview from '@/components/balance/BalancePreview.vue'
|
import BalancePreview from '@/components/balance/BalancePreview.vue'
|
||||||
|
|
@ -117,6 +117,7 @@ import { checkUserVip } from '@/utils/appUtils'
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const showPayment = inject('showPayment')
|
const showPayment = inject('showPayment')
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const previewRef = ref()
|
const previewRef = ref()
|
||||||
const layoutRef = ref()
|
const layoutRef = ref()
|
||||||
|
|
@ -186,7 +187,11 @@ onMounted(async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录'); return }
|
if (!userStore.isLoggedIn) {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
router.replace({ query: { ...route.query, showLogin: 1 } })
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const defaultTitle = originalTitle.value || balanceSettings.pageTitle || '未命名作品'
|
const defaultTitle = originalTitle.value || balanceSettings.pageTitle || '未命名作品'
|
||||||
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
||||||
|
|
@ -298,7 +303,11 @@ async function handleSave() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
if (!userStore.isLoggedIn) {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
router.replace({ query: { ...route.query, showLogin: 1 } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 关键防护:触发导出前强制等待最新的后台用户信息校验完成(防篡改本地缓存与时间差)
|
// 关键防护:触发导出前强制等待最新的后台用户信息校验完成(防篡改本地缓存与时间差)
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -131,14 +131,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="activeTab === 'edit'" class="chat-config-layout" style="display: flex; gap: 32px; height: 540px;">
|
<div v-show="activeTab === 'edit'" class="chat-config-layout" style="display: flex; gap: 32px; height: 100%;">
|
||||||
<div class="msg-list-col" style="flex: 3; display: flex; flex-direction: column;">
|
<div class="msg-list-col" style="flex: 3; display: flex; flex-direction: column;">
|
||||||
<div class="section-label" style="flex-shrink: 0;margin-bottom: 8px;">消息设置</div>
|
<div class="section-label" style="flex-shrink: 0;margin-bottom: 8px;">消息设置</div>
|
||||||
<div class="tip-text"
|
<div class="tip-text"
|
||||||
style="font-size: 12px; color: #999; margin-bottom: 8px;margin-top: 8px; flex-shrink: 0;">长按可以快捷排序哟
|
style="font-size: 12px; color: #999; margin-bottom: 8px;margin-top: 8px; flex-shrink: 0;">长按可以快捷排序哟
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="msg-list-scroll-wrap" style="flex: 1; overflow-y: auto; padding-right: 4px; margin-bottom: 16px;">
|
<div class="msg-list-scroll-wrap"
|
||||||
|
style="flex: 1; overflow-y: auto; padding-right: 4px; margin-bottom: 16px;height: 100%;">
|
||||||
<div class="msg-list">
|
<div class="msg-list">
|
||||||
<div v-for="(msg, idx) in messages" :key="msg.id" class="msg-item"
|
<div v-for="(msg, idx) in messages" :key="msg.id" class="msg-item"
|
||||||
:class="{ active: selectedIdx === idx }" @click="selectedIdx = idx">
|
:class="{ active: selectedIdx === idx }" @click="selectedIdx = idx">
|
||||||
|
|
@ -160,7 +161,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="add-msg-box" style="flex-shrink: 0;">
|
<div class="add-msg-box" style="flex-shrink: 0;margin-bottom: 12px;">
|
||||||
<el-popover placement="top" :width="80" trigger="click" popper-class="custom-chat-popover">
|
<el-popover placement="top" :width="80" trigger="click" popper-class="custom-chat-popover">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button
|
<el-button
|
||||||
|
|
@ -742,7 +743,11 @@ async function handleWallpaperUpload(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录'); return }
|
if (!userStore.isLoggedIn) {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
router.replace({ query: { ...route.query, showLogin: 1 } })
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const defaultTitle = originalTitle.value || settings.title || '未命名作品'
|
const defaultTitle = originalTitle.value || settings.title || '未命名作品'
|
||||||
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
||||||
|
|
@ -852,7 +857,11 @@ async function handleSave() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
if (!userStore.isLoggedIn) {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
router.replace({ query: { ...route.query, showLogin: 1 } })
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!userStore.isVip) {
|
if (!userStore.isVip) {
|
||||||
showPayment?.()
|
showPayment?.()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { resolve } from 'path'
|
||||||
import { batiaoPlugin } from '@batiao/batiao-sdk-vue-vite/onBuild'
|
import { batiaoPlugin } from '@batiao/batiao-sdk-vue-vite/onBuild'
|
||||||
import proxy from './src/proxy.cjs'
|
import proxy from './src/proxy.cjs'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
const copyPublicPlugin = () => {
|
const copyPublicPlugin = () => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -24,8 +25,64 @@ const copyPublicPlugin = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seoInjectionPlugin = () => {
|
||||||
|
return {
|
||||||
|
name: 'seo-injection',
|
||||||
|
async transformIndexHtml(html, ctx) {
|
||||||
|
if (ctx.originalUrl && ctx.originalUrl.includes('/article-detail')) {
|
||||||
|
const urlObj = new URL(ctx.originalUrl, 'http://localhost')
|
||||||
|
const id = urlObj.searchParams.get('id')
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`http://wechat.wenyitu.com/api/articles/${id}`)
|
||||||
|
const article = res.data
|
||||||
|
if (article) {
|
||||||
|
let newHtml = html
|
||||||
|
if (article.title) {
|
||||||
|
newHtml = newHtml.replace(/<title>.*?<\/title>/, `<title>${article.title} - 装样大师</title>`)
|
||||||
|
}
|
||||||
|
if (article.seo_keywords) {
|
||||||
|
newHtml = newHtml.replace(/<meta name="keywords" content="[^"]*"/, `<meta name="keywords" content="${article.seo_keywords}"`)
|
||||||
|
}
|
||||||
|
if (article.seo_description) {
|
||||||
|
newHtml = newHtml.replace(/<meta name="description" content="[^"]*"/, `<meta name="description" content="${article.seo_description}"`)
|
||||||
|
}
|
||||||
|
return newHtml
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[seo-injection] 获取详情失败:', e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ctx.originalUrl && ctx.originalUrl.includes('/article') && ctx.originalUrl.includes('category_id=')) {
|
||||||
|
const urlObj = new URL(ctx.originalUrl, 'http://localhost')
|
||||||
|
const categoryId = urlObj.searchParams.get('category_id')
|
||||||
|
if (categoryId) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`http://wechat.wenyitu.com/api/articles?category_id=${categoryId}&page=1&per_page=10`)
|
||||||
|
const data = res.data
|
||||||
|
if (data && data.items && data.items.length > 0 && data.items[0].category) {
|
||||||
|
let newHtml = html
|
||||||
|
const category = data.items[0].category
|
||||||
|
if (category.seo_keywords) {
|
||||||
|
newHtml = newHtml.replace(/<meta name="keywords" content="[^"]*"/, `<meta name="keywords" content="${category.seo_keywords}"`)
|
||||||
|
}
|
||||||
|
if (category.seo_description) {
|
||||||
|
newHtml = newHtml.replace(/<meta name="description" content="[^"]*"/, `<meta name="description" content="${category.seo_description}"`)
|
||||||
|
}
|
||||||
|
return newHtml
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[seo-injection] 获取列表分类失败:', e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), batiaoPlugin(), copyPublicPlugin()],
|
plugins: [vue(), batiaoPlugin(), copyPublicPlugin(), seoInjectionPlugin()],
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue