对接文章接口

This commit is contained in:
tangxinyue 2026-07-29 16:15:40 +08:00
parent 6a108992e2
commit 5efeb4cb89
19 changed files with 904 additions and 271 deletions

View File

@ -5,6 +5,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>装样大师</title>
<meta name="keywords" content="装样大师, 模拟器" />
<meta name="description" content="装样大师是一款高保真的模拟工具" />
</head>
<body>

12
package-lock.json generated
View File

@ -18,6 +18,7 @@
"express": "^5.2.1",
"html2canvas": "^1.4.1",
"http-proxy": "^1.18.1",
"marked": "^18.0.7",
"mime": "^4.1.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
@ -2621,6 +2622,17 @@
"@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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

View File

@ -10,7 +10,7 @@
"start": "node --stack-size=12800 --stack-trace-limit=20 ./service/main.cjs"
},
"dependencies": {
"@batiao/batiao-sdk-vue-vite": "^1.0.357",
"@batiao/batiao-sdk-vue-vite": "^1.0.386",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.8",
"body-parser": "^2.3.0",
@ -20,6 +20,7 @@
"express": "^5.2.1",
"html2canvas": "^1.4.1",
"http-proxy": "^1.18.1",
"marked": "^18.0.7",
"mime": "^4.1.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",

View File

@ -3,6 +3,8 @@ const routes = require('./routes.cjs')
const httpProxy = require('http-proxy')
const proxy = httpProxy.createProxyServer({})
const fs = require('fs')
const url = require('url')
const axios = require('axios')
const dist = 'dist'
let mime
import('mime').then((res) => {
@ -105,11 +107,46 @@ const matchFile = async (request, response) => {
}
const endIndex = async (request, response) => {
console.log(dist + '/index.html')
fs.readFile(dist + '/index.html', (err, html) => {
fs.readFile(dist + '/index.html', 'utf-8', async (err, html) => {
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.end(html)
response.end(finalHtml)
logger.info(`${request.method} ${httpOk} ${request.url}`)
}
})

View File

@ -50,6 +50,13 @@ export default {
return await get(appConfig.baseURL, "/user", data);
},
/**
* 6. 退出登录
*/
async logout(data = {}) {
return await postJson(appConfig.baseURL, "/user/logout", data);
},
/**
* 获取商品列表
* @param {*} data

48
src/api/wechat.js Normal file
View File

@ -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

View File

@ -1,5 +1,5 @@
<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">
<img class="feature-img" :src="icon" />
<div class="text-col">
@ -12,7 +12,7 @@
</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" />
<div class="tool-text">
<span class="tool-title"> {{ title }} </span>
@ -40,7 +40,7 @@ defineProps({
default: 'small'
}
})
defineEmits(['click'])
defineEmits(['itemClick'])
</script>
<style lang="scss" scoped>

View File

@ -76,7 +76,9 @@
<div class="app-download-wrapper">
<div class="content-box">
<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>
</BaseDialog>
@ -102,9 +104,12 @@ const showAppCode = ref(false)
const appCodeImg = ref('')
const appCodeLoading = ref(false)
async function openAppCodeDialog() {
showAppCode.value = true
if (!appCodeImg.value) {
appCodeLoading.value = true
try {
const res = await authService.getDownloadCode()
console.log('获取APP二维码响应结果:', res)
@ -112,6 +117,8 @@ async function openAppCodeDialog() {
appCodeImg.value = data.android_qrcode
} catch (e) {
console.log(e.message)
} finally {
appCodeLoading.value = false
}
}
}
@ -155,11 +162,11 @@ const navList = [
key: 'entertainment',
children: [
{ name: '工资单', key: 'gongzidan', path: '/gongzidan', icon: '/src/public/header/zhengjianmoni.png' },
{ name: '飞机票', key: 'flight', path: '/flight', icon: '/src/public/header/feijipiao.png' },
{ name: '火车票', key: 'train', path: '/train', icon: '/src/public/header/huochepiao.png' },
{ name: '购物详情', key: 'shopping', path: '/shopping', icon: '/src/public/header/gouwuxiangqing.png' },
{ name: '情侣证', key: 'couple', path: '/couple', icon: '/src/public/header/qinglvzheng.png' },
{ name: '视频聊天', key: 'video', path: '/video', icon: '/src/public/header/shipinliaotian.png' }
{ name: '飞机票', key: 'flight', path: '', icon: '/src/public/header/feijipiao.png' },
{ name: '火车票', key: 'train', path: '', icon: '/src/public/header/huochepiao.png' },
{ name: '购物详情', key: 'shopping', path: '', icon: '/src/public/header/gouwuxiangqing.png' },
{ name: '情侣证', key: 'couple', path: '', icon: '/src/public/header/qinglvzheng.png' },
{ name: '视频聊天', key: 'video', path: '', icon: '/src/public/header/shipinliaotian.png' }
]
},
{ name: '作品库', key: 'works', path: '/works' },
@ -178,6 +185,10 @@ function handleNavClick(item) {
if (item.path) {
router.push(item.path)
}
if (!item.path) {
ElMessage.warning("该功能暂未上线")
return
}
}
function isItemActive(item) {
@ -191,7 +202,7 @@ function isItemActive(item) {
async function handleCommand(cmd) {
if (cmd === 'logout') {
try { await api.auth.logout() } catch { }
try { await authService.logout() } catch { }
userStore.reset()
localStorage.removeItem('token')
localStorage.removeItem('user_info')
@ -223,7 +234,7 @@ function openLoginDialog() {
left: 0;
right: 0;
margin: 0 auto;
max-width: 2000px;
//max-width: 2000px;
min-width: 1200px;
background: #fff;
box-shadow: 0 1px 8px rgba(0, 0, 0, .08);

View File

@ -190,8 +190,12 @@ defineExpose({ previewAreaRef })
.panel-scroll {
flex: 1;
overflow-y: auto;
max-height: 580px;
max-height: 100%;
padding: 18px 68px 0;
.setting-list {
height: 100%
}
}
}

View File

@ -4,9 +4,9 @@ module.exports = {
changeOrigin: true,
rewrite: (path) => path
},
'/static': {
'/wechat-api': {
target: 'http://wechat.wenyitu.com',
changeOrigin: true,
rewrite: (path) => path
rewrite: (path) => path.replace(/^\/wechat-api/, '')
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -182,6 +182,14 @@ function statusLabel(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>
<style lang="scss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<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">
<!-- 顶部返回按钮 -->
@ -27,37 +27,16 @@
</div>
<!-- 文章导读区 -->
<div class="article-intro-box">
<div v-if="currentArticle.intro" class="article-intro-box">
<div class="intro-header">
<img class="star-icon" src="@/public/article/start.png" />
<span>文章导读</span>
</div>
<p class="intro-content">{{ currentArticle.intro || currentArticle.description }}</p>
<p class="intro-content">{{ currentArticle.intro }}</p>
</div>
<!-- 正文区块 -->
<div class="article-body-content">
<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 class="article-body-content markdown-body" v-html="parsedContent">
</div>
<!-- 上一篇 / 下一篇 导航卡片 -->
@ -100,7 +79,7 @@
@click="handleSwitchArticle(rec)">
<div class="rec-rank" :class="{ 'top-rank': index < 3, 'four-rank': index == 3 }">{{ index +
1
}}</div>
}}</div>
<div class="rec-content">
<h4 class="rec-title" :title="rec.title">{{ rec.title }}</h4>
<div class="rec-meta">
@ -120,130 +99,156 @@
</div>
</div>
<!-- 底部警告组件 -->
<WarningFooter />
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import WarningFooter from '@/components/common/WarningFooter.vue'
import { wechatApi } from '@/api/wechat'
import { marked } from 'marked'
const router = useRouter()
const route = useRoute()
// 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) {
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() {
router.push('/article')
}
@ -294,7 +299,7 @@ function handleSwitchArticle(articleItem) {
}
}
@media (max-width: 1180px) {
@media (max-width: 1340px) {
.recommend-sidebar {
display: none;
/* 屏幕过窄时隐藏侧边栏避免挡住主卡片 */
@ -400,36 +405,33 @@ function handleSwitchArticle(articleItem) {
/* 正文 */
.article-body-content {
.section-block {
margin-bottom: 30px;
}
margin-top: 32px;
font-size: 16px;
color: #1A1A1A;
line-height: 26px;
}
.section-title {
font-size: 16px;
line-height: 26px;
font-weight: 700;
color: #1A1A1A;
}
.article-body-content :deep(img) {
max-width: 100%;
border-radius: 8px;
margin: 16px 0;
}
.section-text {
font-size: 16px;
color: #1A1A1A;
line-height: 26px;
}
.article-body-content :deep(h1),
.article-body-content :deep(h2),
.article-body-content :deep(h3) {
color: #1A1A1A;
margin-top: 24px;
margin-bottom: 16px;
}
.article-image-box {
width: 100%;
height: 320px;
border-radius: 4px;
overflow: hidden;
margin-bottom: 30px;
.article-body-content :deep(p) {
margin-bottom: 16px;
}
.article-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.article-body-content :deep(a) {
color: #8B70FF;
text-decoration: none;
}
/* 上一篇/下一篇 导航卡片 */
@ -650,4 +652,8 @@ function handleSwitchArticle(articleItem) {
width: 100%;
}
}
:deep(.el-loading-mask) {
z-index: 1 !important;
}
</style>

View File

@ -1,39 +1,69 @@
<template>
<div class="article-page-wrapper">
<div class="article-list-container">
<div v-for="item in articleList" :key="item.id" class="article-card" @click="handleCardClick(item)">
<!-- 卡片头部信息 -->
<div class="card-header">
<div class="header-left">
<h2 class="article-title" :title="item.title">{{ item.title }}</h2>
<div v-if="item.isHot" class="hot-badge">
<img class="hot-icon" src="@/public/article/hot.png" alt="">
<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 class="card-header">
<div class="header-left">
<h1 class="article-title" :title="item.title">{{ item.title }}</h1>
<div v-if="item.isHot" class="hot-badge">
<img class="hot-icon" src="@/public/article/hot.png" alt="">
</div>
</div>
<div class="header-meta">
<div class="meta-item">
<img class="meta-icon" src="@/public/article/date.png" alt="">
<span>{{ item.date }}</span>
</div>
<div class="meta-item">
<img class="meta-icon" src="@/public/article/eye.png" alt="">
<span>{{ item.views }}</span>
</div>
</div>
</div>
<div class="header-meta">
<div class="meta-item">
<img class="meta-icon" src="@/public/article/date.png" alt="">
<span>{{ item.date }}</span>
<!-- 卡片主体内容 -->
<div class="card-body">
<div class="cover-thumb">
<img :src="item.cover || '/src/public/article/empty.png'" :alt="item.title"
class="cover-img" />
</div>
<div class="meta-item">
<img class="meta-icon" src="@/public/article/eye.png" alt="">
<span>{{ item.views }}</span>
<div class="content-right">
<p class="article-desc">{{ item.description }}</p>
<div class="action-box">
<button class="detail-btn" @click.stop="handleDetail(item)">查看详情</button>
</div>
</div>
</div>
</div>
<!-- 卡片主体内容 -->
<div class="card-body">
<div class="cover-thumb">
<img v-if="item.cover" :src="item.cover" :alt="item.title" class="cover-img" />
<div v-else class="cover-placeholder"></div>
</div>
<!-- 暂无数据提示 -->
<el-empty v-if="!loading && articleList.length === 0" description="暂无文章" image-size="200"
image="/src/public/profile/empty.png" />
<div class="content-right">
<p class="article-desc">{{ item.description }}</p>
<div class="action-box">
<button class="detail-btn" @click.stop="handleDetail(item)">查看详情</button>
<!-- 分页组件 -->
<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>
@ -46,51 +76,164 @@
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ref, onMounted, watch, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import WarningFooter from '@/components/common/WarningFooter.vue'
import { wechatApi } from '@/api/wechat'
const router = useRouter()
const route = useRoute()
//
const articleList = ref([
{
id: 1,
title: '微商模拟神器',
isHot: true,
date: '2026-7-20',
views: '11.5K',
cover: '',
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
},
{
id: 2,
title: '微商模拟神器哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈好...',
isHot: true,
date: '2026-7-20',
views: '11.5K',
cover: '',
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈...'
},
{
id: 1,
title: '微商模拟神器',
isHot: true,
date: '2026-7-20',
views: '11.5K',
cover: '',
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
},
{
id: 2,
title: '微商模拟神器哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈好哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈',
isHot: true,
date: '2026-7-20',
views: '11.5K',
cover: '',
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈...'
//
const loading = ref(false)
const categoryList = ref([{ id: '', name: '全部文章' }])
const currentCategory = ref('')
const articleList = ref([])
//
const queryParams = ref({
page: 1,
per_page: 10,
category_id: '',
keyword: ''
})
const total = ref(0)
//
const currentCategoryName = computed(() => {
const cat = categoryList.value.find(c => c.id === currentCategory.value)
return cat ? cat.name : '全部文章'
})
//
watch(currentCategoryName, (newName) => {
if (newName !== '全部文章') {
document.title = `${newName}-装样大师`
} else {
document.title = `装样大师`
}
])
}, { 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) {
router.push({
@ -115,16 +258,34 @@ function handleCardClick(item) {
box-sizing: border-box;
}
.article-list-container {
.main-container {
width: 100%;
max-width: 820px;
max-width: 1140px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
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 {
width: 800px;
width: 100%;
background: #FFFFFF;
border-radius: 16px;
padding: 34px;
@ -307,4 +468,161 @@ function handleCardClick(item) {
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>

View File

@ -1,10 +1,23 @@
<template>
<div class="div">
<div class="flexcontainer">
<div class="banner-box">
<img class="icon left-icon" src="@/public/index/left.png" alt="">
<img class="icon right-icon" src="@/public/index/right.png" alt="">
<img class="banner" src="@/public/index/banner-1.jpg" alt="" mode="widthFix">
<div class="banner-box" v-loading="loading" style="min-height: 450px;">
<!-- 数据加载完毕且只有一张图时直接显示 -->
<template v-if="!loading && bannerList.length === 1">
<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 class="rectangle_33_1">
<span class="div_3">
@ -31,7 +44,7 @@
<span class="text_26"> 万物皆可模拟 认真你就输了毕业证机票工资单想要啥有啥恶搞到底而且像到亲妈都信 </span>
<div class="tools-grid">
<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>
@ -55,19 +68,81 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted, resolveDirective } from 'vue'
import AppFooter from '@/components/common/AppFooter.vue'
import ToolCard from '@/components/ToolCard.vue'
import { ElMessage } from "element-plus";
import { useRouter, useRoute } from 'vue-router'
import { wechatApi } from '@/api/wechat'
const router = useRouter()
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 = [
{ icon: "/src/public/index/gongzidan.png", title: "工资单", desc: "月入五万截图保真" },
{ icon: "/src/public/index/huochepiao.png", title: "火车票", desc: "说走就走的「假旅行」" },
{ icon: "/src/public/index/feijipiao.png", title: "飞机票", desc: "票比真的还像" },
{ icon: "/src/public/index/gouwumoni.png", title: "购物模拟", desc: "假装清空购物车" },
{ icon: "/src/public/index/zhengjian.png", title: "证件", desc: "防伪底纹都还原" },
{ icon: "/src/public/index/liaotianmoni.png", title: "视频群聊", desc: "视频通话中…" }
{ icon: "/src/public/index/gongzidan.png", title: "工资单", desc: "月入五万截图保真", path: "/gongzidan" },
{ icon: "/src/public/index/huochepiao.png", title: "火车票", desc: "说走就走的「假旅行」", path: "" },
{ icon: "/src/public/index/feijipiao.png", title: "飞机票", desc: "票比真的还像", path: "" },
{ icon: "/src/public/index/gouwumoni.png", title: "购物模拟", desc: "假装清空购物车", path: "" },
{ icon: "/src/public/index/zhengjian.png", title: "证件", desc: "防伪底纹都还原", path: "" },
{ icon: "/src/public/index/liaotianmoni.png", title: "视频群聊", desc: "视频通话中…", path: "" }
]
const serviceList = [
@ -90,6 +165,10 @@ const serviceList = [
]
const handleToolCardClick = (path) => {
if (!path) {
ElMessage.warning("该功能暂未上线")
return
}
router.push(path)
}
</script>
@ -540,4 +619,24 @@ const handleToolCardClick = (path) => {
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>

View File

@ -220,8 +220,9 @@ const randomGenerate = () => {
}
const saveDraft = async () => {
if (!localStorage.getItem('token')) {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.replace({ query: { ...route.query, showLogin: 1 } })
return
}
try {
@ -321,7 +322,11 @@ const saveDraft = 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) {
showPayment?.()
return

View File

@ -104,7 +104,7 @@
<script setup>
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 { FolderAdd, Download } from '@element-plus/icons-vue'
import BalancePreview from '@/components/balance/BalancePreview.vue'
@ -117,6 +117,7 @@ import { checkUserVip } from '@/utils/appUtils'
const userStore = useUserStore()
const showPayment = inject('showPayment')
const route = useRoute()
const router = useRouter()
const previewRef = ref()
const layoutRef = ref()
@ -186,7 +187,11 @@ onMounted(async () => {
})
async function handleSave() {
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录'); return }
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.replace({ query: { ...route.query, showLogin: 1 } })
return
}
try {
const defaultTitle = originalTitle.value || balanceSettings.pageTitle || '未命名作品'
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
@ -298,7 +303,11 @@ async function handleSave() {
}
async function handleExport() {
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.replace({ query: { ...route.query, showLogin: 1 } })
return
}
//
try {

View File

@ -131,14 +131,15 @@
</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="section-label" style="flex-shrink: 0;margin-bottom: 8px;">消息设置</div>
<div class="tip-text"
style="font-size: 12px; color: #999; margin-bottom: 8px;margin-top: 8px; flex-shrink: 0;">长按可以快捷排序哟
</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 v-for="(msg, idx) in messages" :key="msg.id" class="msg-item"
:class="{ active: selectedIdx === idx }" @click="selectedIdx = idx">
@ -160,7 +161,7 @@
</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">
<template #reference>
<el-button
@ -742,7 +743,11 @@ async function handleWallpaperUpload(event) {
}
async function handleSave() {
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录'); return }
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.replace({ query: { ...route.query, showLogin: 1 } })
return
}
try {
const defaultTitle = originalTitle.value || settings.title || '未命名作品'
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
@ -852,7 +857,11 @@ async function handleSave() {
}
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) {
showPayment?.()
return

View File

@ -4,6 +4,7 @@ import { resolve } from 'path'
import { batiaoPlugin } from '@batiao/batiao-sdk-vue-vite/onBuild'
import proxy from './src/proxy.cjs'
import fs from 'fs'
import axios from 'axios'
const copyPublicPlugin = () => {
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({
plugins: [vue(), batiaoPlugin(), copyPublicPlugin()],
plugins: [vue(), batiaoPlugin(), copyPublicPlugin(), seoInjectionPlugin()],
css: {
preprocessorOptions: {
scss: {