移动端首页整合完毕

This commit is contained in:
GeekMaster
2025-08-04 15:04:06 +08:00
parent e994060e93
commit 6c35c69ed7
12 changed files with 1686 additions and 1769 deletions

View File

@@ -324,11 +324,7 @@ const routes = [
name: 'mobile-chat-session',
component: () => import('@/views/mobile/ChatSession.vue'),
},
{
path: '/mobile/chat/export',
name: 'mobile-chat-export',
component: () => import('@/views/mobile/ChatExport.vue'),
},
{
path: '/mobile/apps',
name: 'mobile-apps',

View File

@@ -1,124 +1,144 @@
<template>
<div class="chat-export" v-loading="loading">
<div class="chat-box" id="chat-box">
<div class="title pt-4">
<h2>{{ chatTitle }}</h2>
</div>
<div v-for="item in chatData" :key="item.id">
<chat-prompt v-if="item.type === 'prompt'" :data="item" list-style="list" />
<chat-reply
v-else-if="item.type === 'reply'"
:data="item"
:read-only="true"
list-style="list"
/>
</div>
</div>
<!-- end chat box -->
</div>
</template>
<script setup>
import ChatPrompt from '@/components/ChatPrompt.vue'
import ChatReply from '@/components/ChatReply.vue'
import { httpGet } from '@/utils/http'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import { nextTick, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const chatData = ref([])
const router = useRouter()
const chatId = router.currentRoute.value.query['chat_id']
const loading = ref(true)
const chatTitle = ref('')
httpGet('/api/chat/history?chat_id=' + chatId)
.then((res) => {
const data = res.data
if (!data) {
loading.value = false
return
}
for (let i = 0; i < data.length; i++) {
if (data[i].type === 'prompt') {
chatData.value.push(data[i])
continue
} else if (data[i].type === 'mj') {
data[i].content = JSON.parse(data[i].content)
data[i].content.content = data[i].content?.content
chatData.value.push(data[i])
continue
}
data[i].orgContent = data[i].content
data[i].content = data[i].content
chatData.value.push(data[i])
}
nextTick(() => {
hl.configure({ ignoreUnescapedHTML: true })
const blocks = document.querySelector('#chat-box').querySelectorAll('pre code')
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
loading.value = false
})
.catch((e) => {
ElMessage.error('加载聊天记录失败:' + e.message)
})
httpGet('/api/chat/detail?chat_id=' + chatId)
.then((res) => {
chatTitle.value = res.data.title
})
.catch((e) => {
ElMessage.error('加载会失败: ' + e.message)
})
onMounted(() => {
const clipboard = new Clipboard('.copy-reply')
clipboard.on('success', () => {
ElMessage.success('复制成功!')
})
clipboard.on('error', () => {
ElMessage.error('复制失败!')
})
})
</script>
<style lang="scss">
.chat-export {
display: flex;
justify-content: center;
padding: 0 20px;
.chat-box {
width: 100%;
// 变量定义
--content-font-size: 16px;
--content-color: #c1c1c1;
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
padding: 0 0 50px 0;
.title {
text-align: center;
}
.chat-line {
font-size: 14px;
display: flex;
align-items: center;
.chat-line-inner {
max-width: 800px;
}
}
}
}
</style>
<template>
<div class="chat-export" v-loading="loading">
<div class="chat-box" id="chat-box">
<div class="title pt-4">
<h2>{{ chatTitle }}</h2>
</div>
<div v-for="item in chatData" :key="item.id">
<chat-prompt v-if="item.type === 'prompt'" :data="item" list-style="list" />
<chat-reply
v-else-if="item.type === 'reply'"
:data="item"
:read-only="true"
list-style="list"
/>
</div>
</div>
<!-- end chat box -->
</div>
</template>
<script setup>
import ChatPrompt from '@/components/ChatPrompt.vue'
import ChatReply from '@/components/ChatReply.vue'
import { httpGet } from '@/utils/http'
import Clipboard from 'clipboard'
import { ElMessage } from 'element-plus'
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import { nextTick, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const chatData = ref([])
const router = useRouter()
const chatId = router.currentRoute.value.query['chat_id']
const loading = ref(true)
const chatTitle = ref('')
httpGet('/api/chat/history?chat_id=' + chatId)
.then((res) => {
const data = res.data
if (!data) {
loading.value = false
return
}
for (let i = 0; i < data.length; i++) {
if (data[i].type === 'prompt') {
chatData.value.push(data[i])
continue
} else if (data[i].type === 'mj') {
data[i].content = JSON.parse(data[i].content)
data[i].content.content = data[i].content?.content
chatData.value.push(data[i])
continue
}
data[i].orgContent = data[i].content
data[i].content = data[i].content
chatData.value.push(data[i])
}
nextTick(() => {
hl.configure({ ignoreUnescapedHTML: true })
const blocks = document.querySelector('#chat-box').querySelectorAll('pre code')
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
loading.value = false
})
.catch((e) => {
ElMessage.error('加载聊天记录失败:' + e.message)
})
httpGet('/api/chat/detail?chat_id=' + chatId)
.then((res) => {
chatTitle.value = res.data.title
})
.catch((e) => {
ElMessage.error('加载会失败: ' + e.message)
})
onMounted(() => {
const clipboard = new Clipboard('.copy-reply')
clipboard.on('success', () => {
ElMessage.success('复制成功!')
})
clipboard.on('error', () => {
ElMessage.error('复制失败!')
})
})
</script>
<style lang="scss">
.chat-export {
display: flex;
justify-content: center;
padding: 0 20px;
.chat-box {
width: 100%;
max-width: 800px;
// 变量定义
--content-font-size: 16px;
--content-color: #c1c1c1;
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
padding: 0 0 50px 0;
.title {
text-align: center;
}
.chat-line {
font-size: 14px;
display: flex;
align-items: center;
.chat-line-inner {
max-width: 800px;
}
}
}
}
// 移动端适配
@media (max-width: 768px) {
.chat-export {
padding: 0 10px;
.chat-box {
padding: 0 0 30px 0;
.title h2 {
font-size: 18px;
}
.chat-line {
font-size: 13px;
}
}
}
}
</style>

View File

@@ -1,182 +0,0 @@
<template>
<div class="chat-export-mobile">
<div class="chat-box">
<van-nav-bar left-arrow left-text="返回" @click-left="router.back()">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role }}</van-cell>
<van-cell center title="模型">{{ model }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
</van-nav-bar>
<div class="chat-list-wrapper">
<div id="message-list-box" class="message-list-box">
<van-list
v-model:error="error"
:finished="finished"
error-text="请求失败点击重新加载"
@load="onLoad"
>
<van-cell v-for="item in chatData" :key="item" :border="false" class="message-line">
<chat-prompt
v-if="item.type === 'prompt'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:tokens="item['tokens']"
/>
<chat-reply
v-else-if="item.type === 'reply'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:org-content="item.orgContent"
:tokens="item['tokens']"
/>
</van-cell>
</van-list>
</div>
</div>
</div>
<!-- end chat box -->
</div>
</template>
<script setup>
import ChatPrompt from '@/components/mobile/ChatPrompt.vue'
import ChatReply from '@/components/mobile/ChatReply.vue'
import { httpGet } from '@/utils/http'
import { dateFormat, processContent } from '@/utils/libs'
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import MarkdownIt from 'markdown-it'
import mathjaxPlugin from 'markdown-it-mathjax3'
import { showFailToast } from 'vant'
import { nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
const chatData = ref([])
const router = useRouter()
const chatId = router.currentRoute.value.query['chat_id']
const title = ref('')
const role = ref('')
const model = ref('')
const finished = ref(false)
const error = ref(false)
const md = new MarkdownIt({
breaks: true,
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
// 显示复制代码按钮
const copyBtn = `<span class="copy-code-mobile" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
'&lt;/textarea>'
)}</textarea>`
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`
// 处理代码高亮
const preCode = hl.highlight(str, { language: lang }).value
// 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
}
// 处理代码高亮
const preCode = md.utils.escapeHtml(str)
// 将代码包裹在 pre 中
return `<pre class="code-container">${code}${copyBtn}</pre>`
},
})
md.use(mathjaxPlugin)
const onLoad = () => {
httpGet('/api/chat/history?chat_id=' + chatId)
.then((res) => {
// 加载状态结束
finished.value = true
const data = res.data
if (data && data.length > 0) {
for (let i = 0; i < data.length; i++) {
if (data[i].type === 'prompt') {
chatData.value.push(data[i])
continue
}
data[i].orgContent = data[i].content
data[i].content = md.render(processContent(data[i].content))
chatData.value.push(data[i])
}
nextTick(() => {
hl.configure({ ignoreUnescapedHTML: true })
const blocks = document.querySelector('#message-list-box').querySelectorAll('pre code')
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
}
})
.catch(() => {
error.value = true
})
httpGet(`/api/chat/detail?chat_id=${chatId}`)
.then((res) => {
title.value = res.data.title
model.value = res.data.model
role.value = res.data.role_name
})
.catch((e) => {
showFailToast('加载对话失败:' + e.message)
})
}
</script>
<style scoped lang="scss">
.chat-export-mobile {
height: 100vh;
.van-nav-bar {
position: static;
}
.chat-box {
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
background: #f5f5f5;
.chat-list-wrapper {
padding: 10px 0 10px 0;
.message-list-box {
background: #f5f5f5;
padding-bottom: 50px;
.van-cell {
background: none;
}
}
}
.van-nav-bar__title {
.van-dropdown-menu__title {
margin-right: 10px;
}
.van-cell__title {
text-align: left;
}
}
.van-nav-bar__right {
.van-icon {
font-size: 20px;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<template>
<div class="create-center">
<div class="create-content px-3">
<div class="create-content p-3">
<CustomTabs
:model-value="activeTab"
@update:model-value="activeTab = $event"

View File

@@ -1,81 +1,41 @@
<template>
<div class="discover-page">
<div class="discover-content">
<!-- 功能分类 -->
<div class="category-section">
<h3 class="category-title">AI 工具</h3>
<van-row :gutter="12">
<van-col :span="6" v-for="tool in aiTools" :key="tool.key">
<div class="tool-card" @click="navigateTo(tool.url)">
<div class="tool-icon" :style="{ backgroundColor: tool.color }">
<i class="iconfont" :class="tool.icon"></i>
<!-- AI工具列表 -->
<div class="tools-section">
<div class="section-header">
<h2 class="section-title">AI 创作工具</h2>
<p class="section-subtitle">探索强大的AI创作能力</p>
</div>
<div class="tools-grid">
<div
v-for="tool in aiTools"
:key="tool.key"
class="tool-item"
@click="navigateTo(tool.url)"
>
<div class="tool-card">
<div class="tool-icon-wrapper">
<div class="tool-icon" :style="{ background: tool.gradient }">
<i class="iconfont" :class="tool.icon"></i>
</div>
<div class="tool-badge" v-if="tool.badge">{{ tool.badge }}</div>
</div>
<div class="tool-name">{{ tool.name }}</div>
</div>
</van-col>
</van-row>
</div>
<!-- 用户服务 -->
<div class="category-section">
<h3 class="category-title">我的服务</h3>
<van-cell-group inset>
<van-cell
v-for="service in userServices"
:key="service.key"
:title="service.name"
:value="service.desc"
:icon="service.icon"
is-link
@click="navigateTo(service.url)"
>
<template #icon>
<i class="iconfont" :class="service.icon" :style="{ color: service.color }"></i>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 实用功能 -->
<div class="category-section">
<h3 class="category-title">实用功能</h3>
<van-cell-group inset>
<van-cell
v-for="utility in utilities"
:key="utility.key"
:title="utility.name"
:value="utility.desc"
is-link
@click="navigateTo(utility.url)"
>
<template #icon>
<i class="iconfont" :class="utility.icon" :style="{ color: utility.color }"></i>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 推荐内容 -->
<div class="category-section">
<h3 class="category-title">精选推荐</h3>
<van-grid :column-num="2" :gutter="12" :border="false">
<van-grid-item
v-for="item in recommendations"
:key="item.key"
@click="navigateTo(item.url)"
class="recommend-item"
>
<div class="recommend-card">
<div class="recommend-image">
<van-image :src="item.image" fit="cover" />
<div class="tool-content">
<h3 class="tool-name">{{ tool.name }}</h3>
<p class="tool-desc">{{ tool.desc }}</p>
<div class="tool-meta">
<span class="tool-tag" v-if="tool.tag">{{ tool.tag }}</span>
<span class="tool-status" :class="tool.status">{{ tool.statusText }}</span>
</div>
</div>
<div class="recommend-info">
<div class="recommend-title">{{ item.title }}</div>
<div class="recommend-desc">{{ item.desc }}</div>
<div class="tool-arrow">
<van-icon name="arrow" />
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</div>
</div>
</div>
@@ -89,147 +49,95 @@ const router = useRouter()
// AI工具配置
const aiTools = ref([
{ key: 'mj', name: 'MJ绘画', icon: 'icon-mj', color: '#8B5CF6', url: '/mobile/create?tab=mj' },
{ key: 'sd', name: 'SD绘画', icon: 'icon-sd', color: '#06B6D4', url: '/mobile/create?tab=sd' },
{
key: 'mj',
name: 'MJ绘画',
desc: 'Midjourney AI绘画创作',
icon: 'icon-mj',
gradient: 'linear-gradient(135deg, #8B5CF6, #A855F7)',
badge: '热门',
tag: 'AI绘画',
status: 'active',
statusText: '可用',
url: '/mobile/create?tab=mj',
},
{
key: 'sd',
name: 'SD绘画',
desc: 'Stable Diffusion本地化',
icon: 'icon-sd',
gradient: 'linear-gradient(135deg, #06B6D4, #0891B2)',
tag: 'AI绘画',
status: 'active',
statusText: '可用',
url: '/mobile/create?tab=sd',
},
{
key: 'dalle',
name: 'DALL·E',
desc: 'OpenAI图像生成',
icon: 'icon-dalle',
color: '#F59E0B',
gradient: 'linear-gradient(135deg, #F59E0B, #D97706)',
tag: 'AI绘画',
status: 'active',
statusText: '可用',
url: '/mobile/create?tab=dalle',
},
{
key: 'suno',
name: '音乐创作',
desc: 'AI音乐生成与编辑',
icon: 'icon-music',
color: '#EF4444',
gradient: 'linear-gradient(135deg, #EF4444, #DC2626)',
badge: '新功能',
tag: 'AI音乐',
status: 'active',
statusText: '可用',
url: '/mobile/create?tab=suno',
},
{
key: 'video',
name: '视频生成',
desc: 'AI视频创作工具',
icon: 'icon-video',
color: '#10B981',
gradient: 'linear-gradient(135deg, #10B981, #059669)',
tag: 'AI视频',
status: 'beta',
statusText: '测试版',
url: '/mobile/create?tab=video',
},
{
key: 'jimeng',
name: '即梦AI',
desc: '即梦AI绘画平台',
icon: 'icon-jimeng',
color: '#F97316',
gradient: 'linear-gradient(135deg, #F97316, #EA580C)',
tag: 'AI绘画',
status: 'active',
statusText: '可用',
url: '/mobile/create?tab=jimeng',
},
{
key: 'xmind',
name: '思维导图',
desc: 'AI思维导图生成',
icon: 'icon-mind',
color: '#3B82F6',
gradient: 'linear-gradient(135deg, #3B82F6, #2563EB)',
tag: 'AI工具',
status: 'active',
statusText: '可用',
url: '/mobile/tools?tab=xmind',
},
{ key: 'apps', name: '应用中心', icon: 'icon-apps', color: '#EC4899', url: '/mobile/apps' },
])
// 用户服务
const userServices = ref([
{
key: 'member',
name: '会员中心',
desc: '充值升级享受更多权益',
icon: 'icon-vip',
color: '#FFD700',
url: '/mobile/member',
},
{
key: 'powerLog',
name: '消费记录',
desc: '查看算力使用详情',
icon: 'icon-history',
color: '#10B981',
url: '/mobile/power-log',
},
{
key: 'invite',
name: '邀请好友',
desc: '推广获取奖励',
icon: 'icon-user-plus',
color: '#F59E0B',
url: '/mobile/invite',
},
{
key: 'export',
name: '导出对话',
desc: '保存聊天记录',
icon: 'icon-download',
color: '#06B6D4',
url: '/mobile/chat/export',
},
])
// 实用功能
const utilities = ref([
{
key: 'imgWall',
name: '作品展示',
desc: '浏览精美AI作品',
icon: 'icon-gallery',
color: '#EC4899',
url: '/mobile/imgWall',
},
{
key: 'settings',
name: '设置中心',
desc: '个性化配置',
icon: 'icon-setting',
color: '#6B7280',
url: '/mobile/settings',
},
{
key: 'help',
name: '帮助中心',
desc: '使用指南和常见问题',
icon: 'icon-help',
color: '#8B5CF6',
url: '/mobile/help',
},
{
key: 'feedback',
name: '意见反馈',
desc: '提出建议和问题',
icon: 'icon-message',
color: '#EF4444',
url: '/mobile/feedback',
},
])
// 推荐内容
const recommendations = ref([
{
key: 'new-features',
title: '新功能发布',
desc: '体验最新AI创作工具',
image: '/images/recommend/new-features.jpg',
url: '/mobile/news',
},
{
key: 'tutorials',
title: '使用教程',
desc: '快速上手AI创作',
image: '/images/recommend/tutorials.jpg',
url: '/mobile/tutorials',
},
{
key: 'gallery',
title: '精选作品',
desc: '欣赏优秀AI作品',
image: '/images/recommend/gallery.jpg',
url: '/mobile/imgWall',
},
{
key: 'community',
title: '用户社区',
desc: '交流创作心得',
image: '/images/recommend/community.jpg',
url: '/mobile/community',
key: 'apps',
name: '应用中心',
desc: '更多AI应用工具',
icon: 'icon-apps',
gradient: 'linear-gradient(135deg, #EC4899, #DB2777)',
tag: '应用',
status: 'active',
statusText: '可用',
url: '/mobile/apps',
},
])
@@ -248,132 +156,185 @@ const navigateTo = (url) => {
min-height: 100vh;
background: var(--van-background);
.nav-left {
display: flex;
align-items: center;
.iconfont {
font-size: 20px;
color: var(--van-primary-color);
}
}
.discover-content {
.category-section {
margin-bottom: 24px;
padding: 20px 16px;
.category-title {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 16px 0;
padding-left: 4px;
}
}
// AI工具卡片
.tool-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
background: var(--van-cell-background);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
&:active {
transform: scale(0.95);
}
.tool-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.iconfont {
font-size: 22px;
color: white;
}
}
.tool-name {
font-size: 12px;
font-weight: 500;
color: var(--van-text-color);
.tools-section {
.section-header {
text-align: center;
}
}
margin-bottom: 32px;
// 服务列表
:deep(.van-cell-group) {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.van-cell {
padding: 16px;
.van-cell__title {
font-weight: 500;
.section-title {
font-size: 24px;
font-weight: 700;
color: var(--van-text-color);
margin: 0 0 8px 0;
background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.van-cell__value {
.section-subtitle {
font-size: 14px;
color: var(--van-gray-6);
font-size: 13px;
}
.iconfont {
font-size: 20px;
margin-right: 12px;
margin: 0;
}
}
}
// 推荐内容
.recommend-item {
.recommend-card {
background: var(--van-cell-background);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
.tools-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
&:active {
transform: scale(0.98);
}
.tool-item {
.tool-card {
background: var(--van-cell-background);
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
.recommend-image {
height: 120px;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--van-primary-color), #8b5cf6);
transform: scaleX(0);
transition: transform 0.3s ease;
}
:deep(.van-image) {
width: 100%;
height: 100%;
}
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
.recommend-info {
padding: 12px;
&::before {
transform: scaleX(1);
}
.recommend-title {
font-size: 14px;
font-weight: 600;
color: var(--van-text-color);
margin-bottom: 4px;
}
.tool-icon {
transform: scale(1.1);
}
}
.recommend-desc {
font-size: 12px;
color: var(--van-gray-6);
line-height: 1.4;
&:active {
transform: translateY(-2px);
}
.tool-icon-wrapper {
position: relative;
margin-bottom: 16px;
.tool-icon {
width: 56px;
height: 56px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
transition: transform 0.3s ease;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
.iconfont {
font-size: 28px;
color: white;
}
}
.tool-badge {
position: absolute;
top: -8px;
right: -8px;
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
font-size: 10px;
font-weight: 600;
padding: 4px 8px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
}
}
.tool-content {
text-align: center;
margin-bottom: 16px;
.tool-name {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 6px 0;
}
.tool-desc {
font-size: 12px;
color: var(--van-gray-6);
margin: 0 0 12px 0;
line-height: 1.4;
}
.tool-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.tool-tag {
background: var(--van-primary-color);
color: white;
font-size: 10px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
}
.tool-status {
font-size: 10px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
&.active {
background: #10b981;
color: white;
}
&.beta {
background: #f59e0b;
color: white;
}
&.maintenance {
background: #6b7280;
color: white;
}
}
}
}
.tool-arrow {
position: absolute;
top: 20px;
right: 20px;
color: var(--van-gray-4);
transition: all 0.3s ease;
.van-icon {
font-size: 16px;
}
}
&:hover .tool-arrow {
color: var(--van-primary-color);
transform: translateX(2px);
}
}
}
}
@@ -385,19 +346,61 @@ const navigateTo = (url) => {
:deep(.van-theme-dark) {
.discover-page {
.tool-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
&:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
}
.tool-icon {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
}
}
}
.van-cell-group {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
// 响应式优化
@media (max-width: 375px) {
.discover-page {
.discover-content {
padding: 16px 12px;
.recommend-item .recommend-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
.tools-section {
.section-header {
margin-bottom: 24px;
.section-title {
font-size: 22px;
}
}
.tools-grid {
gap: 12px;
.tool-item .tool-card {
padding: 16px;
.tool-icon-wrapper .tool-icon {
width: 48px;
height: 48px;
.iconfont {
font-size: 24px;
}
}
.tool-content {
.tool-name {
font-size: 15px;
}
.tool-desc {
font-size: 11px;
}
}
}
}
}
}
}
}

View File

@@ -149,7 +149,7 @@ const features = ref([
{
key: 'suno',
name: '音乐创作',
icon: 'icon-music',
icon: 'icon-mp3',
color: '#EF4444',
url: '/mobile/create?tab=suno',
},
@@ -170,14 +170,14 @@ const features = ref([
{
key: 'xmind',
name: '思维导图',
icon: 'icon-mind',
icon: 'icon-xmind',
color: '#3B82F6',
url: '/mobile/tools?tab=xmind',
},
{
key: 'imgWall',
name: '作品展示',
icon: 'icon-gallery',
icon: 'icon-image-list',
color: '#EC4899',
url: '/mobile/imgWall',
},

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@
<!-- 用户状态卡片 -->
<div class="status-cards" v-if="isLogin">
<van-row :gutter="12">
<van-col :span="8">
<van-col :span="12">
<div class="status-card" @click="router.push('/mobile/power-log')">
<div class="card-icon power">
<i class="iconfont icon-flash"></i>
@@ -40,16 +40,7 @@
<div class="card-label">剩余算力</div>
</div>
</van-col>
<van-col :span="8">
<div class="status-card" @click="router.push('/mobile/member')">
<div class="card-icon member">
<i class="iconfont icon-vip"></i>
</div>
<div class="card-value">{{ vipDays }}</div>
<div class="card-label">VIP天数</div>
</div>
</van-col>
<van-col :span="8">
<van-col :span="12">
<div class="status-card" @click="router.push('/mobile/invite')">
<div class="card-icon invite">
<i class="iconfont icon-user-plus"></i>
@@ -68,9 +59,9 @@
<van-col :span="6">
<div class="action-item" @click="router.push('/mobile/member')">
<div class="action-icon recharge">
<i class="iconfont icon-money"></i>
<i class="iconfont icon-vip"></i>
</div>
<div class="action-label">充值</div>
<div class="action-label">会员中心</div>
</div>
</van-col>
<van-col :span="6">
@@ -100,6 +91,28 @@
</van-row>
</div>
<!-- 账户管理 -->
<div class="menu-section" v-if="isLogin">
<h3 class="section-title">账户管理</h3>
<van-cell-group inset>
<van-cell title="绑定邮箱" is-link @click="showBindEmailDialog = true">
<template #icon>
<i class="iconfont icon-email menu-icon"></i>
</template>
</van-cell>
<van-cell title="绑定手机" is-link @click="showBindMobileDialog = true">
<template #icon>
<i class="iconfont icon-mobile menu-icon"></i>
</template>
</van-cell>
<van-cell title="第三方登录" is-link @click="showThirdLoginDialog = true">
<template #icon>
<i class="iconfont icon-login menu-icon"></i>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 功能菜单 -->
<div class="menu-section">
<h3 class="section-title">我的服务</h3>
@@ -114,16 +127,6 @@
<i class="iconfont icon-history menu-icon"></i>
</template>
</van-cell>
<van-cell
title="会员中心"
icon="diamond-o"
is-link
@click="router.push('/mobile/member')"
>
<template #icon>
<i class="iconfont icon-vip menu-icon"></i>
</template>
</van-cell>
<van-cell
title="邀请好友"
icon="friends-o"
@@ -134,12 +137,7 @@
<i class="iconfont icon-user-plus menu-icon"></i>
</template>
</van-cell>
<van-cell
title="聊天导出"
icon="down"
is-link
@click="router.push('/mobile/chat/export')"
>
<van-cell title="聊天导出" icon="down" is-link @click="copyChatExportLink">
<template #icon>
<i class="iconfont icon-download menu-icon"></i>
</template>
@@ -294,6 +292,11 @@
</div>
</van-dialog>
<!-- 组件弹窗 -->
<bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false" />
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false" />
<third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false" />
<!-- 退出登录确认 -->
<van-dialog
:model-value="showLogoutConfirm"
@@ -303,15 +306,24 @@
show-cancel-button
@confirm="logout"
/>
<!-- 隐藏的复制链接按钮 -->
<button id="copy-chat-export-btn" style="display: none" :data-clipboard-text="chatExportUrl">
复制聊天导出链接
</button>
</div>
</template>
<script setup>
import BindEmail from '@/components/BindEmail.vue'
import BindMobile from '@/components/BindMobile.vue'
import ThirdLogin from '@/components/ThirdLogin.vue'
import { checkSession, getSystemInfo } from '@/store/cache'
import { removeUserToken } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs'
import Clipboard from 'clipboard'
import { showFailToast, showLoadingToast, showNotify, showSuccessToast } from 'vant'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -337,6 +349,9 @@ const router = useRouter()
const isLogin = ref(false)
const showSettings = ref(false)
const showPasswordDialog = ref(false)
const showBindEmailDialog = ref(false)
const showBindMobileDialog = ref(false)
const showThirdLoginDialog = ref(false)
const showAvatarOptions = ref(false)
const showAbout = ref(false)
const showLogoutConfirm = ref(false)
@@ -346,6 +361,9 @@ const dark = ref(store.theme === 'dark')
const title = ref(import.meta.env.VITE_TITLE)
const appVersion = ref('2.1.0')
// 聊天导出链接
const chatExportUrl = ref(location.protocol + '//' + location.host + '/chat/export')
// 新增状态
const notifications = ref(true)
const autoSave = ref(true)
@@ -372,13 +390,6 @@ const isVip = computed(() => {
return expiredTime > now
})
const vipDays = computed(() => {
if (!isVip.value) return 0
const now = Date.now()
const expiredTime = form.value.expired_time * 1000
return Math.ceil((expiredTime - now) / (24 * 60 * 60 * 1000))
})
onMounted(() => {
getSystemInfo()
.then((res) => {
@@ -401,6 +412,16 @@ onMounted(() => {
.catch(() => {
isLogin.value = false
})
// 初始化复制功能
const clipboard = new Clipboard('#copy-chat-export-btn')
clipboard.on('success', (e) => {
e.clearSelection()
showNotify({ type: 'success', message: '链接已复制到剪贴板', duration: 2000 })
})
clipboard.on('error', () => {
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
})
})
// 获取用户详细信息
@@ -421,6 +442,11 @@ const fetchUserStats = () => {
inviteCount.value = Math.floor(Math.random() * 20)
}
// 复制聊天导出链接
const copyChatExportLink = () => {
document.getElementById('copy-chat-export-btn').click()
}
// 确认密码验证
const validateConfirmPassword = (value) => {
if (value !== pass.value.new) {
@@ -670,9 +696,9 @@ const logout = function () {
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
@@ -682,16 +708,12 @@ const logout = function () {
background: linear-gradient(135deg, #ff9500, #ff6b35);
}
&.member {
background: linear-gradient(135deg, #ffd700, #ffb300);
}
&.invite {
background: linear-gradient(135deg, #1989fa, #0d7dff);
}
.iconfont {
font-size: 20px;
font-size: 24px;
color: white;
}
}
@@ -748,7 +770,7 @@ const logout = function () {
margin-bottom: 8px;
&.recharge {
background: linear-gradient(135deg, #07c160, #00a550);
background: linear-gradient(135deg, #ffd700, #ffb300);
}
&.password {

View File

@@ -84,7 +84,7 @@
</van-cell-group>
</van-form>
<h3>任务列表</h3>
<h3 class="m-3">任务列表</h3>
<div class="running-job-list pt-3 pb-3">
<van-empty
v-if="runningJobs.length === 0"
@@ -116,7 +116,7 @@
</van-grid>
</div>
<h3>创作记录</h3>
<h3 class="m-3">创作记录</h3>
<div class="finish-job-list">
<van-empty
v-if="finishedJobs.length === 0"

View File

@@ -204,7 +204,7 @@
</div>
</van-form>
<h3>任务列表</h3>
<h3 class="m-3">任务列表</h3>
<div class="running-job-list pt-3 pb-3">
<van-empty
v-if="runningJobs.length === 0"
@@ -236,7 +236,7 @@
</van-grid>
</div>
<h3>创作记录</h3>
<h3 class="m-3">创作记录</h3>
<div class="finish-job-list">
<van-empty
v-if="finishedJobs.length === 0"
@@ -255,7 +255,7 @@
@load="onLoad"
>
<van-grid :gutter="10" :column-num="2">
<van-grid-item v-for="item in finishedJobs" :key="item.id">
<van-grid-item v-for="item in finishedJobs" :key="item.id" class="min-h-[270px]">
<div class="failed" v-if="item.progress === 101">
<div class="title">任务失败</div>
<div class="opt">

View File

@@ -140,7 +140,7 @@
</van-cell-group>
</van-form>
<h3>任务列表</h3>
<h3 class="m-3">任务列表</h3>
<div class="running-job-list pt-3 pb-3">
<van-empty
v-if="runningJobs.length === 0"
@@ -172,7 +172,7 @@
</van-grid>
</div>
<h3>创作记录</h3>
<h3 class="m-3">创作记录</h3>
<div class="finish-job-list">
<van-empty
v-if="finishedJobs.length === 0"