移动端重构第一版

This commit is contained in:
GeekMaster
2025-08-02 11:17:18 +08:00
parent 92915f7678
commit f7cf992598
14 changed files with 6151 additions and 507 deletions

View File

@@ -5,17 +5,21 @@
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import "@/assets/iconfont/iconfont.css";
import "vant/lib/index.css";
import App from "./App.vue";
import { useThemeStore } from "@/store/theme";
import { createPinia } from "pinia";
import "animate.css/animate.min.css";
import "@/assets/css/tailwind.css";
import '@/assets/css/tailwind.css'
import '@/assets/iconfont/iconfont.css'
import { useThemeStore } from '@/store/theme'
import 'animate.css/animate.min.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createPinia } from 'pinia'
import 'vant/lib/index.css'
import { createApp } from 'vue'
import App from './App.vue'
import '@/assets/css/common.scss'
import '@/assets/css/theme-dark.scss'
import '@/assets/css/theme-light.scss'
import { router } from '@/router'
import {
ActionSheet,
Badge,
@@ -48,6 +52,8 @@ import {
Overlay,
Picker,
Popup,
Radio,
RadioGroup,
Row,
Search,
ShareSheet,
@@ -61,64 +67,64 @@ import {
Tabs,
Tag,
TextEllipsis,
Toast,
Uploader,
} from "vant";
import { router } from "@/router";
import "@/assets/css/theme-dark.scss";
import "@/assets/css/theme-light.scss";
import "@/assets/css/common.scss";
} from 'vant'
const pinia = createPinia();
const themeStore = useThemeStore(pinia); // 使用 theme store
const pinia = createPinia()
const themeStore = useThemeStore(pinia) // 使用 theme store
// 设置初始主题
document.documentElement.setAttribute("data-theme", themeStore.theme);
document.documentElement.setAttribute('data-theme', themeStore.theme)
const app = createApp(App);
app.use(createPinia());
app.use(ConfigProvider);
app.use(Tabbar);
app.use(TabbarItem);
app.use(NavBar);
app.use(Search);
app.use(Cell);
app.use(Image);
app.use(TextEllipsis);
app.use(Notify);
app.use(Picker);
app.use(Popup);
app.use(List);
app.use(Form);
app.use(Field);
app.use(CellGroup);
app.use(Button);
app.use(DropdownMenu);
app.use(Icon);
app.use(DropdownItem);
app.use(Sticky);
app.use(SwipeCell);
app.use(Dialog);
app.use(ShareSheet);
app.use(Switch);
app.use(Uploader);
app.use(Tag);
app.use(Overlay);
app.use(Col);
app.use(Row);
app.use(Slider);
app.use(Badge);
app.use(Collapse);
app.use(CollapseItem);
app.use(Grid);
app.use(GridItem);
app.use(Empty);
app.use(Circle);
app.use(Loading);
app.use(Lazyload);
app.use(ImagePreview);
app.use(Tab);
app.use(Tabs);
app.use(Divider);
app.use(NoticeBar);
app.use(ActionSheet);
app.use(router).use(ElementPlus).mount("#app");
const app = createApp(App)
app.use(createPinia())
app.use(ConfigProvider)
app.use(Tabbar)
app.use(TabbarItem)
app.use(NavBar)
app.use(Search)
app.use(Cell)
app.use(Image)
app.use(TextEllipsis)
app.use(Notify)
app.use(Picker)
app.use(Popup)
app.use(Radio)
app.use(RadioGroup)
app.use(List)
app.use(Form)
app.use(Field)
app.use(CellGroup)
app.use(Button)
app.use(DropdownMenu)
app.use(Icon)
app.use(DropdownItem)
app.use(Sticky)
app.use(SwipeCell)
app.use(Dialog)
app.use(ShareSheet)
app.use(Switch)
app.use(Uploader)
app.use(Tag)
app.use(Overlay)
app.use(Col)
app.use(Row)
app.use(Slider)
app.use(Badge)
app.use(Collapse)
app.use(CollapseItem)
app.use(Grid)
app.use(GridItem)
app.use(Empty)
app.use(Circle)
app.use(Loading)
app.use(Lazyload)
app.use(ImagePreview)
app.use(Tab)
app.use(Tabs)
app.use(Divider)
app.use(NoticeBar)
app.use(ActionSheet)
app.use(Toast)
app.use(router).use(ElementPlus).mount('#app')

View File

@@ -309,15 +309,25 @@ const routes = [
component: () => import('@/views/mobile/ChatList.vue'),
},
{
path: '/mobile/image',
name: 'mobile-image',
component: () => import('@/views/mobile/Image.vue'),
path: '/mobile/create',
name: 'mobile-create',
component: () => import('@/views/mobile/Create.vue'),
},
{
path: '/mobile/discover',
name: 'mobile-discover',
component: () => import('@/views/mobile/Discover.vue'),
},
{
path: '/mobile/profile',
name: 'mobile-profile',
component: () => import('@/views/mobile/Profile.vue'),
},
{
path: '/mobile/member',
name: 'mobile-member',
component: () => import('@/views/mobile/Member.vue'),
},
{
path: '/mobile/imgWall',
name: 'mobile-img-wall',
@@ -338,6 +348,37 @@ const routes = [
name: 'mobile-apps',
component: () => import('@/views/mobile/Apps.vue'),
},
// 新增的功能页面路由
{
path: '/mobile/power-log',
name: 'mobile-power-log',
component: () => import('@/views/mobile/PowerLog.vue'),
},
{
path: '/mobile/invite',
name: 'mobile-invite',
component: () => import('@/views/mobile/Invite.vue'),
},
{
path: '/mobile/tools',
name: 'mobile-tools',
component: () => import('@/views/mobile/Tools.vue'),
},
{
path: '/mobile/settings',
name: 'mobile-settings',
component: () => import('@/views/mobile/Settings.vue'),
},
{
path: '/mobile/help',
name: 'mobile-help',
component: () => import('@/views/mobile/Help.vue'),
},
{
path: '/mobile/feedback',
name: 'mobile-feedback',
component: () => import('@/views/mobile/Feedback.vue'),
},
],
},

View File

@@ -0,0 +1,169 @@
<template>
<div class="create-center">
<van-nav-bar title="AI 创作中心" fixed :safe-area-inset-top="true">
<template #left>
<div class="nav-left">
<i class="iconfont icon-mj"></i>
</div>
</template>
</van-nav-bar>
<div class="create-content">
<van-tabs
v-model:active="activeTab"
animated
sticky
:offset-top="44"
@change="onTabChange"
>
<van-tab title="MJ绘画" name="mj" v-if="activeMenu.mj">
<div class="tab-content">
<image-mj />
</div>
</van-tab>
<van-tab title="SD绘画" name="sd" v-if="activeMenu.sd">
<div class="tab-content">
<image-sd />
</div>
</van-tab>
<van-tab title="DALL·E" name="dalle" v-if="activeMenu.dall">
<div class="tab-content">
<image-dall />
</div>
</van-tab>
<van-tab title="音乐创作" name="suno" v-if="activeMenu.suno">
<div class="tab-content">
<suno-create />
</div>
</van-tab>
<van-tab title="视频生成" name="video" v-if="activeMenu.video">
<div class="tab-content">
<video-create />
</div>
</van-tab>
<van-tab title="即梦AI" name="jimeng" v-if="activeMenu.jimeng">
<div class="tab-content">
<jimeng-create />
</div>
</van-tab>
</van-tabs>
</div>
</div>
</template>
<script setup>
import { httpGet } from '@/utils/http'
import ImageDall from '@/views/mobile/pages/ImageDall.vue'
import ImageMj from '@/views/mobile/pages/ImageMj.vue'
import ImageSd from '@/views/mobile/pages/ImageSd.vue'
import { onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// 临时组件,实际项目中需要创建对应的移动端组件
const SunoCreate = { template: '<div class="placeholder">Suno音乐创作功能开发中...</div>' }
const VideoCreate = { template: '<div class="placeholder">视频生成功能开发中...</div>' }
const JimengCreate = { template: '<div class="placeholder">即梦AI功能开发中...</div>' }
const route = useRoute()
const router = useRouter()
const activeTab = ref('mj')
const menus = ref([])
const activeMenu = ref({
mj: false,
sd: false,
dall: false,
suno: false,
video: false,
jimeng: false,
})
// 监听路由参数变化
watch(() => route.query.tab, (newTab) => {
if (newTab && activeMenu.value[newTab]) {
activeTab.value = newTab
}
}, { immediate: true })
// Tab切换处理
const onTabChange = (name) => {
router.replace({
path: route.path,
query: { ...route.query, tab: name }
})
}
onMounted(() => {
fetchMenus()
})
const fetchMenus = () => {
httpGet('/api/menu/list').then((res) => {
menus.value = res.data
activeMenu.value = {
mj: menus.value.some((item) => item.url === '/mj'),
sd: menus.value.some((item) => item.url === '/sd'),
dall: menus.value.some((item) => item.url === '/dalle'),
suno: menus.value.some((item) => item.url === '/suno'),
video: menus.value.some((item) => item.url === '/video'),
jimeng: menus.value.some((item) => item.url === '/jimeng'),
}
// 如果没有指定tab默认选择第一个可用的
if (!route.query.tab) {
const firstAvailable = Object.keys(activeMenu.value).find(key => activeMenu.value[key])
if (firstAvailable) {
activeTab.value = firstAvailable
}
}
}).catch((e) => {
console.error('获取菜单失败:', e.message)
})
}
</script>
<style lang="scss" scoped>
.create-center {
min-height: 100vh;
background: var(--van-background);
.nav-left {
display: flex;
align-items: center;
.iconfont {
font-size: 20px;
color: var(--van-primary-color);
}
}
.create-content {
padding-top: 44px; // nav-bar height
:deep(.van-tabs__nav) {
background: var(--van-background);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
:deep(.van-tab) {
font-weight: 500;
}
:deep(.van-tab--active) {
font-weight: 600;
}
.tab-content {
min-height: calc(100vh - 88px);
.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
color: var(--van-gray-6);
font-size: 16px;
}
}
}
}
</style>

View File

@@ -0,0 +1,328 @@
<template>
<div class="discover-page">
<van-nav-bar title="发现" fixed :safe-area-inset-top="true">
<template #left>
<div class="nav-left">
<i class="iconfont icon-compass"></i>
</div>
</template>
</van-nav-bar>
<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>
</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>
<div class="recommend-info">
<div class="recommend-title">{{ item.title }}</div>
<div class="recommend-desc">{{ item.desc }}</div>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { ref } from 'vue'
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: 'dalle', name: 'DALL·E', icon: 'icon-dalle', color: '#F59E0B', url: '/mobile/create?tab=dalle' },
{ key: 'suno', name: '音乐创作', icon: 'icon-music', color: '#EF4444', url: '/mobile/create?tab=suno' },
{ key: 'video', name: '视频生成', icon: 'icon-video', color: '#10B981', url: '/mobile/create?tab=video' },
{ key: 'jimeng', name: '即梦AI', icon: 'icon-jimeng', color: '#F97316', url: '/mobile/create?tab=jimeng' },
{ key: 'xmind', name: '思维导图', icon: 'icon-mind', color: '#3B82F6', 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'
}
])
// 导航处理
const navigateTo = (url) => {
if (url.startsWith('http')) {
window.open(url, '_blank')
} else {
router.push(url)
}
}
</script>
<style lang="scss" scoped>
.discover-page {
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 {
padding: 54px 16px 60px; // nav-bar height + bottom padding
.category-section {
margin-bottom: 24px;
.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);
text-align: center;
}
}
// 服务列表
: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;
}
.van-cell__value {
color: var(--van-gray-6);
font-size: 13px;
}
.iconfont {
font-size: 20px;
margin-right: 12px;
}
}
}
// 推荐内容
.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;
&:active {
transform: scale(0.98);
}
.recommend-image {
height: 120px;
overflow: hidden;
:deep(.van-image) {
width: 100%;
height: 100%;
}
}
.recommend-info {
padding: 12px;
.recommend-title {
font-size: 14px;
font-weight: 600;
color: var(--van-text-color);
margin-bottom: 4px;
}
.recommend-desc {
font-size: 12px;
color: var(--van-gray-6);
line-height: 1.4;
}
}
}
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.discover-page {
.tool-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
.tool-icon {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
.van-cell-group {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.recommend-item .recommend-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}
}
</style>

View File

@@ -0,0 +1,177 @@
<template>
<div class="mobile-feedback">
<!-- 顶部导航 -->
<van-nav-bar title="意见反馈" left-arrow @click-left="router.back()" fixed placeholder />
<!-- 反馈表单 -->
<div class="feedback-content">
<!-- 反馈类型 -->
<van-cell-group title="反馈类型">
<van-radio-group v-model="feedbackType" direction="horizontal">
<van-radio name="bug">问题反馈</van-radio>
<van-radio name="feature">功能建议</van-radio>
<van-radio name="other">其他</van-radio>
</van-radio-group>
</van-cell-group>
<!-- 反馈内容 -->
<van-cell-group title="反馈内容">
<van-field
v-model="feedbackContent"
type="textarea"
placeholder="请详细描述您遇到的问题或建议..."
:rows="6"
maxlength="500"
show-word-limit
autosize
/>
</van-cell-group>
<!-- 联系方式 -->
<van-cell-group title="联系方式(选填)">
<van-field v-model="contactInfo" placeholder="邮箱或手机号,方便我们回复您" clearable />
</van-cell-group>
<!-- 图片上传 -->
<van-cell-group title="上传截图(选填)">
<van-uploader
v-model="fileList"
:max-count="3"
:after-read="afterRead"
:before-delete="beforeDelete"
upload-text="上传图片"
/>
</van-cell-group>
<!-- 提交按钮 -->
<div class="submit-section">
<van-button type="primary" block :loading="submitting" @click="submitFeedback">
提交反馈
</van-button>
</div>
</div>
</div>
</template>
<script setup>
import { showSuccessToast, showToast } from 'vant'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 响应式数据
const feedbackType = ref('bug')
const feedbackContent = ref('')
const contactInfo = ref('')
const fileList = ref([])
const submitting = ref(false)
// 上传图片后的处理
const afterRead = (file) => {
// 这里可以处理图片上传逻辑
console.log('上传文件:', file)
}
// 删除图片前的处理
const beforeDelete = (file, detail) => {
// 这里可以处理图片删除逻辑
console.log('删除文件:', file)
return true
}
// 提交反馈
const submitFeedback = async () => {
if (!feedbackContent.value.trim()) {
showToast('请输入反馈内容')
return
}
submitting.value = true
try {
const feedbackData = {
type: feedbackType.value,
content: feedbackContent.value,
contact: contactInfo.value,
images: fileList.value.map((file) => file.url || file.content),
timestamp: new Date().toISOString(),
}
// 暂时使用本地存储保存反馈数据
// 后续可以对接后端API
const existingFeedback = JSON.parse(localStorage.getItem('userFeedback') || '[]')
existingFeedback.push(feedbackData)
localStorage.setItem('userFeedback', JSON.stringify(existingFeedback))
showSuccessToast('反馈提交成功,感谢您的建议!')
// 清空表单
feedbackContent.value = ''
contactInfo.value = ''
fileList.value = []
// 返回上一页
setTimeout(() => {
router.back()
}, 1500)
} catch (error) {
console.error('提交反馈失败:', error)
showToast('提交失败,请稍后重试')
} finally {
submitting.value = false
}
}
</script>
<style lang="scss" scoped>
.mobile-feedback {
min-height: 100vh;
background-color: #f7f8fa;
}
.feedback-content {
padding: 16px;
}
.van-cell-group {
margin-bottom: 16px;
border-radius: 8px;
overflow: hidden;
}
.van-radio-group {
padding: 16px;
display: flex;
gap: 24px;
}
.van-field {
padding: 16px;
}
.submit-section {
margin-top: 32px;
padding: 0 16px;
}
.van-uploader {
padding: 16px;
}
// 自定义样式
:deep(.van-cell-group__title) {
padding: 16px 16px 8px;
font-size: 14px;
font-weight: 500;
color: #323233;
}
:deep(.van-field__control) {
min-height: 120px;
}
:deep(.van-radio__label) {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,846 @@
<template>
<div class="help-page">
<van-nav-bar title="帮助中心" left-arrow @click-left="router.back()" fixed>
<template #right>
<van-icon name="search" @click="showSearch = true" />
</template>
</van-nav-bar>
<div class="help-content">
<!-- 搜索框 -->
<div class="search-section" v-if="showSearch">
<van-search
v-model="searchKeyword"
placeholder="搜索帮助内容"
@search="onSearch"
@cancel="showSearch = false"
show-action
/>
</div>
<!-- 常见问题 -->
<div class="faq-section" v-if="!showSearch">
<h3 class="section-title">常见问题</h3>
<van-collapse v-model="activeNames" accordion>
<van-collapse-item
v-for="faq in frequentFAQs"
:key="faq.id"
:title="faq.question"
:name="faq.id"
class="faq-item"
>
<div class="faq-answer" v-html="faq.answer"></div>
</van-collapse-item>
</van-collapse>
</div>
<!-- 功能指南 -->
<div class="guide-section" v-if="!showSearch">
<h3 class="section-title">功能指南</h3>
<van-grid :column-num="2" :gutter="12" :border="false">
<van-grid-item
v-for="guide in guides"
:key="guide.id"
@click="openGuide(guide)"
class="guide-item"
>
<div class="guide-card">
<div class="guide-icon" :style="{ backgroundColor: guide.color }">
<i class="iconfont" :class="guide.icon"></i>
</div>
<div class="guide-title">{{ guide.title }}</div>
<div class="guide-desc">{{ guide.desc }}</div>
</div>
</van-grid-item>
</van-grid>
</div>
<!-- 问题分类 -->
<div class="category-section" v-if="!showSearch">
<h3 class="section-title">问题分类</h3>
<van-cell-group inset>
<van-cell
v-for="category in categories"
:key="category.id"
:title="category.name"
:value="`${category.count}个问题`"
is-link
@click="openCategory(category)"
>
<template #icon>
<i class="iconfont" :class="category.icon" class="category-icon"></i>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 搜索结果 -->
<div class="search-results" v-if="showSearch && searchResults.length > 0">
<h3 class="section-title">搜索结果</h3>
<van-list>
<van-cell
v-for="result in searchResults"
:key="result.id"
:title="result.title"
@click="openSearchResult(result)"
is-link
>
<template #label>
<div class="search-snippet" v-html="result.snippet"></div>
</template>
</van-cell>
</van-list>
</div>
<!-- 空搜索结果 -->
<van-empty
v-if="showSearch && searchKeyword && searchResults.length === 0"
description="没有找到相关内容"
/>
<!-- 联系客服 -->
<div class="contact-section" v-if="!showSearch">
<h3 class="section-title">联系我们</h3>
<van-cell-group inset>
<van-cell title="在线客服" icon="service-o" is-link @click="openCustomerService">
<template #value>
<span class="online-status">在线</span>
</template>
</van-cell>
<van-cell title="意见反馈" icon="chat-o" is-link @click="router.push('/mobile/feedback')" />
<van-cell title="官方QQ群" icon="friends-o" is-link @click="joinQQGroup">
<template #value>
<span class="qq-number">123456789</span>
</template>
</van-cell>
<van-cell title="官方微信" icon="wechat" is-link @click="showWeChatQR = true" />
</van-cell-group>
</div>
<!-- 使用提示 -->
<div class="tips-section" v-if="!showSearch">
<h3 class="section-title">使用提示</h3>
<van-swipe :autoplay="5000" class="tips-swipe">
<van-swipe-item v-for="tip in tips" :key="tip.id">
<div class="tip-card">
<div class="tip-icon">
<i class="iconfont" :class="tip.icon"></i>
</div>
<h4 class="tip-title">{{ tip.title }}</h4>
<p class="tip-content">{{ tip.content }}</p>
</div>
</van-swipe-item>
</van-swipe>
</div>
</div>
<!-- 帮助详情弹窗 -->
<van-action-sheet v-model:show="showHelpDetail" :title="selectedHelp?.title">
<div class="help-detail" v-if="selectedHelp">
<div class="detail-content" v-html="selectedHelp.content"></div>
<div class="detail-actions">
<van-button @click="likeHelp(selectedHelp)">
<van-icon name="good-job-o" /> 有用
</van-button>
<van-button @click="shareHelp(selectedHelp)">
<van-icon name="share-o" /> 分享
</van-button>
</div>
</div>
</van-action-sheet>
<!-- 微信二维码弹窗 -->
<van-dialog v-model:show="showWeChatQR" title="官方微信" :show-cancel-button="false">
<div class="wechat-qr">
<div class="qr-code">
<img src="/images/wechat-qr.png" alt="微信二维码" @error="onQRError" />
</div>
<p class="qr-tip">扫描二维码添加官方微信</p>
</div>
</van-dialog>
<!-- 客服聊天 -->
<van-action-sheet v-model:show="showCustomerChat" title="在线客服" :close-on-click-overlay="false">
<div class="customer-chat">
<div class="chat-header">
<div class="customer-info">
<van-image src="/images/customer-service.png" round width="40" height="40" />
<div class="customer-detail">
<div class="customer-name">智能客服</div>
<div class="customer-status online">在线</div>
</div>
</div>
<van-button size="small" @click="showCustomerChat = false">结束</van-button>
</div>
<div class="chat-messages" ref="chatMessages">
<div
v-for="message in customerMessages"
:key="message.id"
class="message-item"
:class="{ 'user-message': message.isUser }"
>
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ formatTime(message.time) }}</div>
</div>
</div>
<div class="chat-input">
<van-field
v-model="customerMessage"
placeholder="请输入您的问题..."
@keyup.enter="sendCustomerMessage"
>
<template #button>
<van-button size="small" type="primary" @click="sendCustomerMessage">
发送
</van-button>
</template>
</van-field>
</div>
</div>
</van-action-sheet>
</div>
</template>
<script setup>
import { showNotify, showSuccessToast } from 'vant'
import { nextTick, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const showSearch = ref(false)
const searchKeyword = ref('')
const searchResults = ref([])
const activeNames = ref([])
const selectedHelp = ref(null)
const showHelpDetail = ref(false)
const showWeChatQR = ref(false)
const showCustomerChat = ref(false)
const customerMessage = ref('')
const customerMessages = ref([])
const chatMessages = ref()
// 常见问题
const frequentFAQs = ref([
{
id: 1,
question: '如何获得算力?',
answer: '<p>您可以通过以下方式获得算力:</p><ul><li>注册即送算力</li><li>购买充值套餐</li><li>邀请好友注册</li><li>参与活动获得</li></ul>'
},
{
id: 2,
question: '如何使用AI绘画功能',
answer: '<p>使用AI绘画功能很简单</p><ol><li>进入创作中心</li><li>选择绘画工具MJ、SD、DALL-E</li><li>输入描述文字</li><li>点击生成即可</li></ol>'
},
{
id: 3,
question: '为什么生成失败?',
answer: '<p>生成失败可能的原因:</p><ul><li>算力不足</li><li>内容违规</li><li>网络不稳定</li><li>服务器繁忙</li></ul><p>请检查算力余额并重试。</p>'
},
{
id: 4,
question: '如何成为VIP会员',
answer: '<p>成为VIP会员的方式</p><ol><li>进入会员中心</li><li>选择合适的套餐</li><li>完成支付</li><li>自动开通VIP权限</li></ol>'
},
{
id: 5,
question: '如何导出聊天记录?',
answer: '<p>导出聊天记录步骤:</p><ol><li>进入对话页面</li><li>点击右上角菜单</li><li>选择"导出记录"</li><li>选择导出格式</li><li>确认导出</li></ol>'
}
])
// 功能指南
const guides = ref([
{
id: 1,
title: 'AI对话',
desc: '与AI智能对话',
icon: 'icon-chat',
color: '#1989fa',
content: 'AI对话使用指南详细内容...'
},
{
id: 2,
title: 'AI绘画',
desc: '生成精美图片',
icon: 'icon-mj',
color: '#8B5CF6',
content: 'AI绘画使用指南详细内容...'
},
{
id: 3,
title: 'AI音乐',
desc: '创作美妙音乐',
icon: 'icon-music',
color: '#ee0a24',
content: 'AI音乐创作指南详细内容...'
},
{
id: 4,
title: 'AI视频',
desc: '制作精彩视频',
icon: 'icon-video',
color: '#07c160',
content: 'AI视频制作指南详细内容...'
}
])
// 问题分类
const categories = ref([
{ id: 1, name: '账户问题', icon: 'icon-user', count: 15 },
{ id: 2, name: '功能使用', icon: 'icon-apps', count: 23 },
{ id: 3, name: '充值支付', icon: 'icon-money', count: 12 },
{ id: 4, name: '技术问题', icon: 'icon-setting', count: 18 },
{ id: 5, name: '其他问题', icon: 'icon-help', count: 8 }
])
// 使用提示
const tips = ref([
{
id: 1,
title: '提高绘画质量',
content: '使用详细的描述词可以获得更好的绘画效果,建议加入风格、色彩、构图等关键词。',
icon: 'icon-bulb'
},
{
id: 2,
title: '节省算力',
content: '合理使用不同模型简单问题使用GPT-3.5复杂任务使用GPT-4。',
icon: 'icon-flash'
},
{
id: 3,
title: '快速上手',
content: '查看应用中心的预设角色可以快速体验不同类型的AI对话。',
icon: 'icon-star'
}
])
onMounted(() => {
// 初始化客服消息
customerMessages.value = [
{
id: 1,
content: '您好欢迎使用我们的AI创作平台有什么可以帮助您的吗',
isUser: false,
time: new Date()
}
]
})
// 搜索
const onSearch = (keyword) => {
if (!keyword.trim()) {
searchResults.value = []
return
}
// 模拟搜索结果
const allContent = [
...frequentFAQs.value.map(faq => ({
id: faq.id,
title: faq.question,
content: faq.answer,
type: 'faq'
})),
...guides.value.map(guide => ({
id: guide.id,
title: guide.title,
content: guide.content,
type: 'guide'
}))
]
searchResults.value = allContent
.filter(item =>
item.title.includes(keyword) || item.content.includes(keyword)
)
.map(item => ({
...item,
snippet: getSearchSnippet(item.content, keyword)
}))
}
// 获取搜索摘要
const getSearchSnippet = (content, keyword) => {
const cleanContent = content.replace(/<[^>]*>/g, '')
const index = cleanContent.toLowerCase().indexOf(keyword.toLowerCase())
if (index === -1) return cleanContent.substr(0, 100) + '...'
const start = Math.max(0, index - 50)
const end = Math.min(cleanContent.length, index + keyword.length + 50)
let snippet = cleanContent.substr(start, end - start)
// 高亮关键词
const regex = new RegExp(`(${keyword})`, 'gi')
snippet = snippet.replace(regex, '<mark>$1</mark>')
return (start > 0 ? '...' : '') + snippet + (end < cleanContent.length ? '...' : '')
}
// 打开指南
const openGuide = (guide) => {
selectedHelp.value = {
title: guide.title,
content: guide.content || '<p>该指南内容正在完善中,敬请期待。</p>'
}
showHelpDetail.value = true
}
// 打开分类
const openCategory = (category) => {
showNotify({ type: 'primary', message: `正在加载${category.name}...` })
// 这里可以跳转到分类详情页
}
// 打开搜索结果
const openSearchResult = (result) => {
selectedHelp.value = {
title: result.title,
content: result.content
}
showHelpDetail.value = true
}
// 点赞帮助
const likeHelp = (help) => {
showSuccessToast('感谢您的反馈!')
}
// 分享帮助
const shareHelp = (help) => {
if (navigator.share) {
navigator.share({
title: help.title,
text: help.content.replace(/<[^>]*>/g, ''),
url: window.location.href
})
} else {
showNotify({ type: 'primary', message: '该功能暂不支持' })
}
}
// 打开客服
const openCustomerService = () => {
showCustomerChat.value = true
}
// 发送客服消息
const sendCustomerMessage = () => {
if (!customerMessage.value.trim()) return
// 添加用户消息
customerMessages.value.push({
id: Date.now(),
content: customerMessage.value,
isUser: true,
time: new Date()
})
const userMessage = customerMessage.value
customerMessage.value = ''
// 滚动到底部
nextTick(() => {
if (chatMessages.value) {
chatMessages.value.scrollTop = chatMessages.value.scrollHeight
}
})
// 模拟客服回复
setTimeout(() => {
let reply = '感谢您的问题,我们会尽快为您处理。'
if (userMessage.includes('算力')) {
reply = '关于算力问题,您可以在会员中心购买算力套餐,或者通过邀请好友获得免费算力。'
} else if (userMessage.includes('绘画')) {
reply = '关于AI绘画建议您使用详细的描述词这样可以获得更好的效果。'
} else if (userMessage.includes('充值')) {
reply = '充值问题请您检查支付方式是否正确,如有问题可以联系技术客服。'
}
customerMessages.value.push({
id: Date.now(),
content: reply,
isUser: false,
time: new Date()
})
nextTick(() => {
if (chatMessages.value) {
chatMessages.value.scrollTop = chatMessages.value.scrollHeight
}
})
}, 1000)
}
// 加入QQ群
const joinQQGroup = () => {
// 尝试打开QQ群链接
const qqGroupUrl = 'mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D123456789'
window.location.href = qqGroupUrl
setTimeout(() => {
showNotify({ type: 'primary', message: '请在QQ中搜索群号123456789' })
}, 1000)
}
// 格式化时间
const formatTime = (time) => {
return time.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 二维码加载错误
const onQRError = (e) => {
e.target.src = '/images/default-qr.png'
}
</script>
<style lang="scss" scoped>
.help-page {
min-height: 100vh;
background: var(--van-background);
.help-content {
padding: 54px 16px 20px;
.search-section {
margin-bottom: 20px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 16px 4px;
}
.faq-section {
margin-bottom: 24px;
:deep(.van-collapse-item) {
background: var(--van-cell-background);
border-radius: 12px;
margin-bottom: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.van-collapse-item__title {
padding: 16px;
font-weight: 500;
}
.van-collapse-item__content {
padding: 0 16px 16px;
.faq-answer {
color: var(--van-gray-7);
line-height: 1.6;
:deep(ul), :deep(ol) {
padding-left: 20px;
margin: 8px 0;
}
:deep(li) {
margin: 4px 0;
}
:deep(p) {
margin: 8px 0;
}
}
}
}
}
.guide-section {
margin-bottom: 24px;
.guide-item {
.guide-card {
background: var(--van-cell-background);
border-radius: 12px;
padding: 20px 16px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.guide-icon {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
.iconfont {
font-size: 24px;
color: white;
}
}
.guide-title {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
margin-bottom: 8px;
}
.guide-desc {
font-size: 13px;
color: var(--van-gray-6);
}
}
}
}
.category-section,
.contact-section {
margin-bottom: 24px;
:deep(.van-cell-group) {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.van-cell {
padding: 16px;
.category-icon {
font-size: 18px;
color: var(--van-primary-color);
margin-right: 12px;
}
.van-cell__title {
font-size: 15px;
font-weight: 500;
}
.online-status {
color: #07c160;
font-size: 12px;
}
.qq-number {
color: var(--van-gray-6);
font-size: 13px;
}
}
}
}
.search-results {
.search-snippet {
margin-top: 4px;
color: var(--van-gray-6);
font-size: 13px;
line-height: 1.4;
:deep(mark) {
background: var(--van-primary-color);
color: white;
padding: 1px 2px;
border-radius: 2px;
}
}
}
.tips-section {
.tips-swipe {
height: 140px;
border-radius: 12px;
overflow: hidden;
.tip-card {
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6);
color: white;
padding: 20px;
text-align: center;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
.tip-icon {
margin-bottom: 12px;
.iconfont {
font-size: 28px;
opacity: 0.9;
}
}
.tip-title {
font-size: 16px;
font-weight: 600;
margin: 0 0 8px 0;
}
.tip-content {
font-size: 13px;
opacity: 0.9;
line-height: 1.4;
margin: 0;
}
}
}
}
}
.help-detail {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
.detail-content {
color: var(--van-text-color);
line-height: 1.6;
margin-bottom: 20px;
:deep(p) {
margin: 8px 0;
}
:deep(ul), :deep(ol) {
padding-left: 20px;
margin: 8px 0;
}
}
.detail-actions {
display: flex;
gap: 12px;
.van-button {
flex: 1;
}
}
}
.wechat-qr {
text-align: center;
padding: 20px;
.qr-code {
width: 200px;
height: 200px;
margin: 0 auto 16px;
border: 1px solid var(--van-border-color);
border-radius: 8px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.qr-tip {
font-size: 14px;
color: var(--van-gray-6);
margin: 0;
}
}
.customer-chat {
height: 500px;
display: flex;
flex-direction: column;
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--van-border-color);
.customer-info {
display: flex;
align-items: center;
.customer-detail {
margin-left: 12px;
.customer-name {
font-size: 15px;
font-weight: 500;
color: var(--van-text-color);
}
.customer-status {
font-size: 12px;
&.online {
color: #07c160;
}
}
}
}
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
.message-item {
margin-bottom: 16px;
&.user-message {
text-align: right;
.message-content {
background: var(--van-primary-color);
color: white;
}
}
.message-content {
display: inline-block;
max-width: 80%;
padding: 10px 12px;
background: var(--van-gray-1);
border-radius: 8px;
font-size: 14px;
line-height: 1.4;
}
.message-time {
font-size: 11px;
color: var(--van-gray-5);
margin-top: 4px;
}
}
}
.chat-input {
padding: 16px;
border-top: 1px solid var(--van-border-color);
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.help-page {
.van-collapse-item,
.guide-card,
.van-cell-group {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}
}
</style>

View File

@@ -3,11 +3,37 @@
<div class="mobile-home">
<router-view />
<van-tabbar route v-model="active">
<van-tabbar-item to="/mobile/index" name="home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/mobile/chat" name="chat" icon="chat-o">对话</van-tabbar-item>
<van-tabbar-item to="/mobile/image" name="image" icon="photo-o">绘图</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的 </van-tabbar-item>
<van-tabbar route v-model="active" :safe-area-inset-bottom="true">
<van-tabbar-item to="/mobile/index" name="home" icon="home-o">
<span>首页</span>
<template #icon="props">
<i class="iconfont icon-house" :class="{ 'active-icon': props.active }"></i>
</template>
</van-tabbar-item>
<van-tabbar-item to="/mobile/chat" name="chat" icon="chat-o">
<span>对话</span>
<template #icon="props">
<i class="iconfont icon-chat" :class="{ 'active-icon': props.active }"></i>
</template>
</van-tabbar-item>
<van-tabbar-item to="/mobile/create" name="create" icon="plus">
<span>创作</span>
<template #icon="props">
<i class="iconfont icon-mj" :class="{ 'active-icon': props.active }"></i>
</template>
</van-tabbar-item>
<van-tabbar-item to="/mobile/discover" name="discover" icon="apps-o">
<span>发现</span>
<template #icon="props">
<i class="iconfont icon-more" :class="{ 'active-icon': props.active }"></i>
</template>
</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">
<span>我的</span>
<template #icon="props">
<i class="iconfont icon-user-circle" :class="{ 'active-icon': props.active }"></i>
</template>
</van-tabbar-item>
</van-tabbar>
</div>
</van-config-provider>
@@ -38,9 +64,32 @@ watch(
position: fixed;
width: 100%;
}
padding: 0 10px;
}
.van-tabbar {
box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1);
.van-tabbar-item {
.active-icon {
color: var(--van-primary-color) !important;
transform: scale(1.1);
transition: all 0.3s ease;
}
&--active {
.van-tabbar-item__text {
color: var(--van-primary-color);
font-weight: 600;
}
}
}
.iconfont {
font-size: 20px;
transition: all 0.3s ease;
}
}
}
// 黑色主题

View File

@@ -1,86 +1,150 @@
<template>
<div class="index container">
<div class="header">
<h2 class="title">{{ title }}</h2>
<div class="user-greeting">
<div class="greeting-text">
<h2 class="title">{{ getGreeting() }}</h2>
<p class="subtitle">{{ title }}</p>
</div>
<div class="user-avatar" v-if="isLogin" @click="router.push('profile')">
<van-image :src="userAvatar" round width="40" height="40" />
</div>
<div class="login-btn" v-else @click="showLoginDialog(router)">
<van-button size="small" type="primary" round>登录</van-button>
</div>
</div>
</div>
<div class="content mb-8">
<div class="feature-grid">
<van-grid :column-num="3" :gutter="15" border>
<van-grid-item @click="router.push('chat')" class="feature-item">
<template #icon>
<div class="feature-icon">
<i class="iconfont icon-chat"></i>
<!-- 快捷操作区 -->
<div class="quick-actions mb-6">
<van-row :gutter="12">
<van-col :span="12">
<div class="action-card primary" @click="router.push('chat')">
<div class="action-content">
<i class="iconfont icon-chat action-icon"></i>
<div class="action-text">
<div class="action-title">AI 对话</div>
<div class="action-desc">智能助手随时待命</div>
</div>
</template>
<template #text>
<div class="text">AI 对话</div>
</template>
</van-grid-item>
</div>
</div>
</van-col>
<van-col :span="12">
<div class="action-card secondary" @click="router.push('create')">
<div class="action-content">
<i class="iconfont icon-mj action-icon"></i>
<div class="action-text">
<div class="action-title">AI 创作</div>
<div class="action-desc">图像音视频生成</div>
</div>
</div>
</div>
</van-col>
</van-row>
</div>
<van-grid-item @click="router.push('image')" class="feature-item">
<template #icon>
<div class="feature-icon">
<i class="iconfont icon-mj"></i>
</div>
</template>
<template #text>
<div class="text">AI 绘画</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('imgWall')" class="feature-item">
<template #icon>
<div class="feature-icon">
<van-icon name="photo-o" />
</div>
</template>
<template #text>
<div class="text">AI 画廊</div>
</template>
</van-grid-item>
</van-grid>
<!-- 功能网格 -->
<div class="feature-section mb-6">
<div class="section-header">
<h3 class="section-title">AI 功能</h3>
</div>
<van-grid :column-num="4" :gutter="12" :border="false">
<van-grid-item
v-for="feature in features"
:key="feature.key"
@click="navigateToFeature(feature)"
class="feature-item"
>
<template #icon>
<div class="feature-icon" :style="{ backgroundColor: feature.color }">
<i class="iconfont" :class="feature.icon"></i>
</div>
</template>
<template #text>
<div class="feature-text">{{ feature.name }}</div>
</template>
</van-grid-item>
</van-grid>
</div>
<!-- 推荐应用 -->
<div class="apps-section">
<div class="section-header">
<h3 class="section-title">推荐应用</h3>
<van-button class="more-btn" size="small" icon="arrow" @click="router.push('apps')"
>更多</van-button
<van-button
class="more-btn"
size="small"
icon="arrow"
type="primary"
plain
round
@click="router.push('apps')"
>
更多
</van-button>
</div>
<div class="app-list">
<van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps">
<van-cell v-for="item in displayApps" :key="item.id" class="app-cell">
<div class="app-card">
<div class="app-info">
<div class="app-image">
<van-image :src="item.icon" round />
<van-swipe :autoplay="3000" :show-indicators="false" class="app-swipe">
<van-swipe-item v-for="chunk in appChunks" :key="chunk[0]?.id">
<div class="app-row">
<div
v-for="item in chunk"
:key="item.id"
class="app-item"
@click="useRole(item.id)"
>
<div class="app-avatar">
<van-image :src="item.icon" round fit="cover" />
</div>
<div class="app-detail">
<div class="app-title">{{ item.name }}</div>
<div class="app-desc">{{ item.hello_msg }}</div>
<div class="app-info">
<div class="app-name">{{ item.name }}</div>
<div class="app-desc">{{ item.intro }}</div>
</div>
<div class="app-action">
<van-button
size="mini"
type="primary"
plain
round
@click.stop="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '已添加' : '添加' }}
</van-button>
</div>
</div>
<div class="app-actions">
<van-button size="small" type="primary" class="action-btn" @click="useRole(item.id)"
>对话</van-button
>
<van-button
size="small"
:type="hasRole(item.key) ? 'danger' : 'success'"
class="action-btn"
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '移除' : '添加' }}
</van-button>
</div>
</div>
</van-cell>
</van-list>
</van-swipe-item>
</van-swipe>
</div>
</div>
<!-- 数据统计 -->
<div class="stats-section" v-if="isLogin">
<div class="section-header">
<h3 class="section-title">使用统计</h3>
</div>
<van-row :gutter="12">
<van-col :span="8">
<div class="stat-card">
<div class="stat-number">{{ userStats.conversations || 0 }}</div>
<div class="stat-label">对话次数</div>
</div>
</van-col>
<van-col :span="8">
<div class="stat-card">
<div class="stat-number">{{ userStats.images || 0 }}</div>
<div class="stat-label">生成图片</div>
</div>
</van-col>
<van-col :span="8">
<div class="stat-card">
<div class="stat-number">{{ userStats.power || 0 }}</div>
<div class="stat-label">剩余算力</div>
</div>
</van-col>
</van-row>
</div>
</div>
</template>
@@ -99,20 +163,53 @@ const isLogin = ref(false)
const apps = ref([])
const loading = ref(false)
const roles = ref([])
const slogan = ref('你有多大想象力AI就有多大创造力')
// 只显示前5个应用
const displayApps = computed(() => {
return apps.value.slice(0, 8)
const userAvatar = ref('/images/avatar/default.jpg')
const userStats = ref({
conversations: 0,
images: 0,
power: 0
})
// 功能配置
const features = 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: 'dalle', name: 'DALL·E', icon: 'icon-dalle', color: '#F59E0B', url: '/mobile/create?tab=dalle' },
{ key: 'suno', name: '音乐创作', icon: 'icon-music', color: '#EF4444', url: '/mobile/create?tab=suno' },
{ key: 'video', name: '视频生成', icon: 'icon-video', color: '#10B981', url: '/mobile/create?tab=video' },
{ key: 'jimeng', name: '即梦AI', icon: 'icon-jimeng', color: '#F97316', url: '/mobile/create?tab=jimeng' },
{ key: 'xmind', name: '思维导图', icon: 'icon-mind', color: '#3B82F6', url: '/mobile/tools?tab=xmind' },
{ key: 'imgWall', name: '作品展示', icon: 'icon-gallery', color: '#EC4899', url: '/mobile/imgWall' }
])
// 应用分组显示每行2个
const appChunks = computed(() => {
const chunks = []
const displayApps = apps.value.slice(0, 6) // 只显示前6个
for (let i = 0; i < displayApps.length; i += 2) {
chunks.push(displayApps.slice(i, i + 2))
}
return chunks
})
// 获取问候语
const getGreeting = () => {
const hour = new Date().getHours()
if (hour < 6) return '夜深了'
if (hour < 12) return '早上好'
if (hour < 18) return '下午好'
return '晚上好'
}
// 导航到功能页面
const navigateToFeature = (feature) => {
router.push(feature.url)
}
onMounted(() => {
getSystemInfo()
.then((res) => {
title.value = res.data.title
if (res.data.slogan) {
slogan.value = res.data.slogan
}
})
.catch((e) => {
ElMessage.error('获取系统配置失败:' + e.message)
@@ -122,8 +219,12 @@ onMounted(() => {
.then((user) => {
isLogin.value = true
roles.value = user.chat_roles
userAvatar.value = user.avatar || '/images/avatar/default.jpg'
// 获取用户统计数据
fetchUserStats()
})
.catch(() => {})
fetchApps()
})
@@ -133,7 +234,7 @@ const fetchApps = () => {
const items = res.data
// 处理 hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80)
items[i].intro = substr(items[i].hello_msg, 30)
}
apps.value = items
})
@@ -142,6 +243,22 @@ const fetchApps = () => {
})
}
const fetchUserStats = () => {
if (!isLogin.value) return
// 这里可以调用实际的统计接口
// httpGet('/api/user/stats').then(res => {
// userStats.value = res.data
// })
// 临时使用模拟数据
userStats.value = {
conversations: Math.floor(Math.random() * 100),
images: Math.floor(Math.random() * 50),
power: Math.floor(Math.random() * 1000)
}
}
const updateRole = (row, opt) => {
if (!isLogin.value) {
return showLoginDialog(router)
@@ -165,10 +282,10 @@ const updateRole = (row, opt) => {
}
httpPost('/api/app/update', { keys: roles.value })
.then(() => {
ElMessage.success({ message: title.value + '成功!', duration: 1000 })
showNotify({ type: 'success', message: title.value + '成功!', duration: 1000 })
})
.catch((e) => {
ElMessage.error(title.value + '失败:' + e.message)
showNotify({ type: 'danger', message: title.value + '失败:' + e.message })
})
}
@@ -187,148 +304,299 @@ const useRole = (roleId) => {
<style scoped lang="scss">
.index {
color: var(--van-text-color);
background-color: var(--van-background);
background: linear-gradient(135deg, var(--van-background), var(--van-background-2));
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 0 16px 60px;
.header {
flex-shrink: 0;
padding: 10px 15px;
text-align: center;
background: var(--van-background);
padding: 20px 0 16px;
position: sticky;
top: 0;
z-index: 1;
z-index: 100;
background: inherit;
backdrop-filter: blur(10px);
.title {
font-size: 24px;
font-weight: 600;
color: var(--van-text-color);
}
.user-greeting {
display: flex;
justify-content: space-between;
align-items: center;
.slogan {
font-size: 14px;
color: var(--van-gray-6);
.greeting-text {
flex: 1;
.title {
font-size: 28px;
font-weight: 700;
color: var(--van-text-color);
margin: 0 0 4px 0;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 14px;
color: var(--van-gray-6);
margin: 0;
}
}
.user-avatar,
.login-btn {
flex-shrink: 0;
margin-left: 16px;
}
}
}
.content {
flex: 1;
overflow-y: auto;
padding: 15px;
-webkit-overflow-scrolling: touch;
.quick-actions {
.action-card {
border-radius: 16px;
padding: 20px;
background: var(--van-cell-background);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
cursor: pointer;
.feature-grid {
margin-bottom: 30px;
&:active {
transform: scale(0.98);
}
.feature-item {
padding: 15px 0;
&.primary {
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6);
color: white;
.feature-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--van-primary-color);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
.action-icon,
.action-title,
.action-desc {
color: white;
}
}
i,
.van-icon {
font-size: 24px;
color: white;
}
&.secondary {
background: linear-gradient(135deg, #06B6D4, #10B981);
color: white;
.action-icon,
.action-title,
.action-desc {
color: white;
}
}
.action-content {
display: flex;
align-items: center;
.action-icon {
font-size: 32px;
margin-right: 16px;
opacity: 0.9;
}
.text {
font-size: 14px;
font-weight: 500;
.action-text {
.action-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.action-desc {
font-size: 13px;
opacity: 0.8;
}
}
}
}
}
.feature-section,
.apps-section,
.stats-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
margin-bottom: 16px;
.section-title {
font-size: 18px;
font-weight: 600;
font-size: 20px;
font-weight: 700;
color: var(--van-text-color);
margin: 0;
}
.more-btn {
padding: 0 10px;
padding: 6px 12px;
font-size: 12px;
border-radius: 15px;
}
}
}
.app-list {
.app-cell {
padding: 0;
margin-bottom: 15px;
.feature-section {
.feature-item {
padding: 16px 8px;
transition: all 0.3s ease;
.app-card {
&:active {
transform: scale(0.95);
}
.feature-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
i {
font-size: 24px;
color: white;
}
}
.feature-text {
font-size: 12px;
font-weight: 500;
color: var(--van-text-color);
text-align: center;
}
}
}
.apps-section {
.app-swipe {
margin: 0 -4px;
.app-row {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 4px;
.app-item {
background: var(--van-cell-background);
border-radius: 12px;
padding: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
padding: 16px;
display: flex;
align-items: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
cursor: pointer;
.app-info {
display: flex;
align-items: center;
margin-bottom: 15px;
&:active {
transform: scale(0.98);
}
.app-image {
width: 60px;
height: 60px;
margin-right: 15px;
.app-avatar {
width: 44px;
height: 44px;
margin-right: 12px;
flex-shrink: 0;
:deep(.van-image) {
width: 100%;
height: 100%;
}
}
.app-detail {
flex: 1;
.app-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 5px;
color: var(--van-text-color);
}
.app-desc {
font-size: 13px;
color: var(--van-gray-6);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
:deep(.van-image) {
width: 100%;
height: 100%;
}
}
.app-actions {
display: flex;
gap: 10px;
.app-info {
flex: 1;
min-width: 0;
.action-btn {
flex: 1;
border-radius: 20px;
padding: 0 10px;
.app-name {
font-size: 15px;
font-weight: 600;
color: var(--van-text-color);
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.app-desc {
font-size: 12px;
color: var(--van-gray-6);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.app-action {
flex-shrink: 0;
margin-left: 8px;
}
}
}
}
}
.stats-section {
.stat-card {
background: var(--van-cell-background);
border-radius: 12px;
padding: 16px;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
.stat-number {
font-size: 24px;
font-weight: 700;
color: var(--van-primary-color);
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: var(--van-gray-6);
}
}
}
}
// 响应式调整
@media (max-width: 375px) {
.index {
padding: 0 12px 60px;
.header .user-greeting .greeting-text .title {
font-size: 24px;
}
.quick-actions .action-card {
padding: 16px;
.action-content .action-icon {
font-size: 28px;
margin-right: 12px;
}
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.index {
.quick-actions .action-card {
&.primary,
&.secondary {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
}
.feature-section .feature-item .feature-icon {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.apps-section .app-swipe .app-row .app-item,
.stats-section .stat-card {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
}
}
}
</style>

View File

@@ -0,0 +1,739 @@
<template>
<div class="invite-page">
<van-nav-bar title="邀请好友" left-arrow @click-left="router.back()" fixed />
<div class="invite-content">
<!-- 邀请头图 -->
<div class="invite-header">
<div class="header-bg">
<img src="/images/invite-bg.png" alt="邀请背景" @error="onImageError" />
</div>
<div class="header-content">
<h2 class="invite-title">邀请好友获得奖励</h2>
<p class="invite-desc">邀请好友注册即可获得算力奖励</p>
</div>
</div>
<!-- 用户统计 -->
<div class="stats-section">
<van-row :gutter="12">
<van-col :span="8">
<div class="stat-card">
<div class="stat-number">{{ userStats.inviteCount }}</div>
<div class="stat-label">累计邀请</div>
</div>
</van-col>
<van-col :span="8">
<div class="stat-card">
<div class="stat-number">{{ userStats.rewardTotal }}</div>
<div class="stat-label">获得奖励</div>
</div>
</van-col>
<van-col :span="8">
<div class="stat-card">
<div class="stat-number">{{ userStats.todayInvite }}</div>
<div class="stat-label">今日邀请</div>
</div>
</van-col>
</van-row>
</div>
<!-- 奖励规则 -->
<div class="rules-section">
<h3 class="section-title">奖励规则</h3>
<div class="rules-list">
<div class="rule-item" v-for="rule in rewardRules" :key="rule.id">
<div class="rule-icon">
<i class="iconfont" :class="rule.icon" :style="{ color: rule.color }"></i>
</div>
<div class="rule-content">
<div class="rule-title">{{ rule.title }}</div>
<div class="rule-desc">{{ rule.desc }}</div>
</div>
<div class="rule-reward">
<span class="reward-value">+{{ rule.reward }}</span>
<span class="reward-unit">算力</span>
</div>
</div>
</div>
</div>
<!-- 邀请方式 -->
<div class="invite-methods">
<h3 class="section-title">邀请方式</h3>
<div class="methods-grid">
<div class="method-item" @click="shareToWeChat">
<div class="method-icon wechat">
<i class="iconfont icon-wechat"></i>
</div>
<div class="method-name">微信分享</div>
</div>
<div class="method-item" @click="copyInviteLink">
<div class="method-icon link">
<i class="iconfont icon-link"></i>
</div>
<div class="method-name">复制链接</div>
</div>
<div class="method-item" @click="shareQRCode">
<div class="method-icon qr">
<i class="iconfont icon-qrcode"></i>
</div>
<div class="method-name">二维码</div>
</div>
<div class="method-item" @click="shareToFriends">
<div class="method-icon more">
<i class="iconfont icon-share"></i>
</div>
<div class="method-name">更多</div>
</div>
</div>
</div>
<!-- 邀请码 -->
<div class="invite-code-section">
<div class="code-card">
<div class="code-header">
<span class="code-label">我的邀请码</span>
<van-button size="small" type="primary" plain @click="copyInviteCode">
复制
</van-button>
</div>
<div class="code-value">{{ inviteCode }}</div>
<div class="code-link">
<van-field
v-model="inviteLink"
readonly
placeholder="邀请链接"
>
<template #button>
<van-button size="small" type="primary" @click="copyInviteLink">
复制链接
</van-button>
</template>
</van-field>
</div>
</div>
</div>
<!-- 邀请记录 -->
<div class="invite-records">
<div class="records-header">
<h3 class="section-title">邀请记录</h3>
<van-button size="small" type="primary" plain @click="showAllRecords = !showAllRecords">
{{ showAllRecords ? '收起' : '查看全部' }}
</van-button>
</div>
<div class="records-list">
<van-list
v-model:loading="recordsLoading"
:finished="recordsFinished"
finished-text="没有更多记录"
@load="loadInviteRecords"
>
<div
v-for="record in displayRecords"
:key="record.id"
class="record-item"
>
<div class="record-avatar">
<van-image :src="record.avatar" round width="40" height="40" />
</div>
<div class="record-info">
<div class="record-name">{{ record.username }}</div>
<div class="record-time">{{ formatTime(record.created_at) }}</div>
</div>
<div class="record-status">
<van-tag :type="record.status === 'completed' ? 'success' : 'warning'">
{{ record.status === 'completed' ? '已获得奖励' : '待获得奖励' }}
</van-tag>
</div>
</div>
<van-empty v-if="!recordsLoading && inviteRecords.length === 0" description="暂无邀请记录" />
</van-list>
</div>
</div>
</div>
<!-- 二维码弹窗 -->
<van-dialog
v-model:show="showQRDialog"
title="邀请二维码"
:show-cancel-button="false"
confirm-button-text="保存图片"
@confirm="saveQRCode"
>
<div class="qr-content">
<div ref="qrCodeRef" class="qr-code">
<!-- 这里应该生成实际的二维码 -->
<div class="qr-placeholder">
<i class="iconfont icon-qrcode"></i>
<p>邀请二维码</p>
</div>
</div>
<p class="qr-tip">扫描二维码或长按保存分享给好友</p>
</div>
</van-dialog>
</div>
</template>
<script setup>
import { checkSession } from '@/store/cache'
import { httpGet } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs'
import { showFailToast, showNotify, showSuccessToast } from 'vant'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const userStats = ref({
inviteCount: 0,
rewardTotal: 0,
todayInvite: 0
})
const inviteCode = ref('')
const inviteLink = ref('')
const inviteRecords = ref([])
const recordsLoading = ref(false)
const recordsFinished = ref(false)
const showAllRecords = ref(false)
const showQRDialog = ref(false)
const qrCodeRef = ref()
// 奖励规则配置
const rewardRules = ref([
{
id: 1,
title: '好友注册',
desc: '好友通过邀请链接成功注册',
icon: 'icon-user-plus',
color: '#1989fa',
reward: 50
},
{
id: 2,
title: '好友首次充值',
desc: '好友首次充值任意金额',
icon: 'icon-money',
color: '#07c160',
reward: 100
},
{
id: 3,
title: '好友活跃使用',
desc: '好友连续使用7天',
icon: 'icon-star',
color: '#ff9500',
reward: 200
}
])
// 显示的记录根据showAllRecords决定
const displayRecords = computed(() => {
return showAllRecords.value ? inviteRecords.value : inviteRecords.value.slice(0, 5)
})
onMounted(() => {
initPage()
})
const initPage = async () => {
try {
const user = await checkSession()
// 生成邀请码和链接
inviteCode.value = user.invite_code || generateInviteCode()
inviteLink.value = `${location.origin}/register?invite=${inviteCode.value}`
// 获取用户邀请统计
fetchInviteStats()
// 加载邀请记录
loadInviteRecords()
} catch (error) {
showLoginDialog(router)
}
}
const generateInviteCode = () => {
return Math.random().toString(36).substr(2, 8).toUpperCase()
}
const fetchInviteStats = () => {
// 这里应该调用实际的API
// httpGet('/api/user/invite/stats').then(res => {
// userStats.value = res.data
// })
// 临时使用模拟数据
userStats.value = {
inviteCount: Math.floor(Math.random() * 50),
rewardTotal: Math.floor(Math.random() * 5000),
todayInvite: Math.floor(Math.random() * 5)
}
}
const loadInviteRecords = () => {
if (recordsFinished.value) return
recordsLoading.value = true
// 模拟API调用
setTimeout(() => {
const mockRecords = generateMockRecords()
inviteRecords.value.push(...mockRecords)
recordsLoading.value = false
// 模拟数据加载完成
if (inviteRecords.value.length >= 20) {
recordsFinished.value = true
}
}, 1000)
}
const generateMockRecords = () => {
const records = []
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
for (let i = 0; i < 10; i++) {
records.push({
id: Date.now() + i,
username: names[i % names.length] + (i + 1),
avatar: '/images/avatar/default.jpg',
status: Math.random() > 0.3 ? 'completed' : 'pending',
created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString()
})
}
return records
}
const formatTime = (timeStr) => {
const date = new Date(timeStr)
return date.toLocaleDateString()
}
// 分享到微信
const shareToWeChat = () => {
if (typeof WeixinJSBridge !== 'undefined') {
// 在微信中分享
WeixinJSBridge.invoke('sendAppMessage', {
title: '邀请你使用AI创作平台',
desc: '强大的AI工具让创作更简单',
link: inviteLink.value,
imgUrl: `${location.origin}/images/share-logo.png`
})
} else {
// 复制链接提示
copyInviteLink()
showNotify({ type: 'primary', message: '请在微信中打开链接进行分享' })
}
}
// 复制邀请码
const copyInviteCode = async () => {
try {
await navigator.clipboard.writeText(inviteCode.value)
showSuccessToast('邀请码已复制')
} catch (err) {
// 降级方案
const textArea = document.createElement('textarea')
textArea.value = inviteCode.value
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
showSuccessToast('邀请码已复制')
}
}
// 复制邀请链接
const copyInviteLink = async () => {
try {
await navigator.clipboard.writeText(inviteLink.value)
showSuccessToast('邀请链接已复制')
} catch (err) {
// 降级方案
const textArea = document.createElement('textarea')
textArea.value = inviteLink.value
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
showSuccessToast('邀请链接已复制')
}
}
// 显示二维码
const shareQRCode = () => {
showQRDialog.value = true
}
// 保存二维码
const saveQRCode = () => {
showNotify({ type: 'primary', message: '请长按二维码保存到相册' })
}
// 更多分享方式
const shareToFriends = () => {
if (navigator.share) {
navigator.share({
title: '邀请你使用AI创作平台',
text: '强大的AI工具让创作更简单',
url: inviteLink.value
})
} else {
copyInviteLink()
}
}
// 图片加载错误处理
const onImageError = (e) => {
e.target.src = '/images/default-bg.png'
}
</script>
<style lang="scss" scoped>
.invite-page {
min-height: 100vh;
background: var(--van-background);
.invite-content {
padding-top: 46px;
.invite-header {
position: relative;
height: 200px;
overflow: hidden;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6);
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.3;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.header-content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
color: white;
text-align: center;
.invite-title {
font-size: 24px;
font-weight: 700;
margin: 0 0 8px 0;
}
.invite-desc {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
}
}
.stats-section {
padding: 16px;
margin-top: -20px;
position: relative;
z-index: 3;
.stat-card {
background: var(--van-cell-background);
border-radius: 12px;
padding: 16px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.stat-number {
font-size: 20px;
font-weight: 700;
color: var(--van-primary-color);
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: var(--van-gray-6);
}
}
}
.rules-section,
.invite-methods,
.invite-code-section,
.invite-records {
padding: 0 16px 16px;
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 16px 0;
}
}
.rules-list {
.rule-item {
display: flex;
align-items: center;
background: var(--van-cell-background);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.rule-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(25, 137, 250, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
.iconfont {
font-size: 20px;
}
}
.rule-content {
flex: 1;
.rule-title {
font-size: 15px;
font-weight: 600;
color: var(--van-text-color);
margin-bottom: 4px;
}
.rule-desc {
font-size: 13px;
color: var(--van-gray-6);
}
}
.rule-reward {
text-align: right;
.reward-value {
font-size: 16px;
font-weight: 600;
color: #07c160;
}
.reward-unit {
font-size: 12px;
color: var(--van-gray-6);
margin-left: 2px;
}
}
}
}
.methods-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
.method-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 16px;
background: var(--van-cell-background);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
.method-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
&.wechat {
background: #07c160;
}
&.link {
background: #1989fa;
}
&.qr {
background: #8B5CF6;
}
&.more {
background: #ff9500;
}
.iconfont {
font-size: 20px;
color: white;
}
}
.method-name {
font-size: 12px;
color: var(--van-text-color);
text-align: center;
}
}
}
.invite-code-section {
.code-card {
background: var(--van-cell-background);
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.code-label {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
}
}
.code-value {
font-size: 24px;
font-weight: 700;
color: var(--van-primary-color);
text-align: center;
padding: 16px;
background: rgba(25, 137, 250, 0.1);
border-radius: 8px;
margin-bottom: 16px;
letter-spacing: 2px;
}
}
}
.invite-records {
.records-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.records-list {
.record-item {
display: flex;
align-items: center;
background: var(--van-cell-background);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.record-avatar {
margin-right: 12px;
}
.record-info {
flex: 1;
.record-name {
font-size: 15px;
font-weight: 500;
color: var(--van-text-color);
margin-bottom: 4px;
}
.record-time {
font-size: 12px;
color: var(--van-gray-6);
}
}
}
}
}
}
.qr-content {
text-align: center;
padding: 20px;
.qr-code {
width: 200px;
height: 200px;
margin: 0 auto 16px;
border: 1px solid var(--van-border-color);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: var(--van-background-2);
.qr-placeholder {
text-align: center;
color: var(--van-gray-6);
.iconfont {
font-size: 48px;
margin-bottom: 8px;
}
p {
margin: 0;
font-size: 14px;
}
}
}
.qr-tip {
font-size: 13px;
color: var(--van-gray-6);
margin: 0;
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.invite-page {
.stat-card,
.rule-item,
.method-item,
.code-card,
.record-item {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}
}
</style>

View File

@@ -0,0 +1,608 @@
<template>
<div class="member-center">
<van-nav-bar title="会员中心" left-arrow @click-left="router.back()" fixed>
<template #right>
<van-icon name="question-o" @click="showHelp = true" />
</template>
</van-nav-bar>
<div class="member-content">
<!-- 用户信息卡片 -->
<div class="user-card" v-if="isLogin">
<div class="user-info">
<van-image :src="userInfo.avatar" round width="60" height="60" />
<div class="user-detail">
<div class="user-name">{{ userInfo.nickname || userInfo.username }}</div>
<div class="user-level">
<van-tag :type="vipInfo.isVip ? 'primary' : 'default'" size="medium">
{{ vipInfo.isVip ? 'VIP会员' : '普通用户' }}
</van-tag>
</div>
</div>
</div>
<div class="user-stats">
<div class="stat-item">
<div class="stat-value">{{ userInfo.power || 0 }}</div>
<div class="stat-label">剩余算力</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item" v-if="vipInfo.isVip">
<div class="stat-value">{{ vipInfo.daysLeft }}</div>
<div class="stat-label">VIP剩余</div>
</div>
<div class="stat-item" v-else>
<div class="stat-value">--</div>
<div class="stat-label">VIP到期</div>
</div>
</div>
</div>
<!-- 会员特权 -->
<div class="privileges-section">
<h3 class="section-title">会员特权</h3>
<div class="privileges-grid">
<div
v-for="privilege in privileges"
:key="privilege.key"
class="privilege-item"
:class="{ active: vipInfo.isVip }"
>
<div class="privilege-icon" :style="{ backgroundColor: privilege.color }">
<i class="iconfont" :class="privilege.icon"></i>
</div>
<div class="privilege-info">
<div class="privilege-title">{{ privilege.title }}</div>
<div class="privilege-desc">{{ privilege.desc }}</div>
</div>
<div class="privilege-status">
<van-icon
:name="vipInfo.isVip ? 'success' : 'cross'"
:color="vipInfo.isVip ? '#07c160' : '#ee0a24'"
/>
</div>
</div>
</div>
</div>
<!-- 充值套餐 -->
<div class="packages-section">
<h3 class="section-title">充值套餐</h3>
<div class="packages-list">
<div
v-for="pkg in packages"
:key="pkg.id"
class="package-item"
:class="{ recommended: pkg.recommended }"
@click="selectPackage(pkg)"
>
<div class="package-tag" v-if="pkg.recommended">推荐</div>
<div class="package-header">
<div class="package-name">{{ pkg.name }}</div>
<div class="package-price">
<span class="current-price">{{ pkg.discount || pkg.price }}</span>
<span class="original-price" v-if="pkg.discount">{{ pkg.price }}</span>
</div>
</div>
<div class="package-features">
<div class="feature-item">
<van-icon name="checked" color="#07c160" />
<span>{{ pkg.power }}算力值</span>
</div>
<div class="feature-item" v-if="pkg.days > 0">
<van-icon name="checked" color="#07c160" />
<span>{{ pkg.days }}天有效期</span>
</div>
<div class="feature-item" v-else>
<van-icon name="checked" color="#07c160" />
<span>长期有效</span>
</div>
<div class="feature-item" v-if="pkg.features">
<van-icon name="checked" color="#07c160" />
<span>{{ pkg.features }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 支付方式 -->
<div class="payment-section" v-if="selectedPackage">
<h3 class="section-title">支付方式</h3>
<van-radio-group v-model="selectedPayment">
<van-cell-group inset>
<van-cell
v-for="payment in paymentMethods"
:key="payment.type"
clickable
@click="selectedPayment = payment.type"
>
<template #title>
<div class="payment-info">
<i class="iconfont" :class="payment.icon" :style="{ color: payment.color }"></i>
<span>{{ payment.name }}</span>
</div>
</template>
<template #right-icon>
<van-radio :name="payment.type" />
</template>
</van-cell>
</van-radio-group>
</van-radio-group>
</div>
<!-- 支付按钮 -->
<div class="pay-button" v-if="selectedPackage">
<van-button
type="primary"
size="large"
round
block
:loading="payLoading"
@click="processPay"
>
立即支付 {{ selectedPackage.discount || selectedPackage.price }}
</van-button>
</div>
</div>
<!-- 帮助弹窗 -->
<van-action-sheet v-model:show="showHelp" title="会员帮助">
<div class="help-content">
<div class="help-item">
<h4>什么是算力</h4>
<p>算力是使用AI功能时消耗的虚拟货币不同功能消耗的算力不同</p>
</div>
<div class="help-item">
<h4>如何获得算力</h4>
<p>通过充值套餐可以获得算力会员用户还可享受每月赠送的算力</p>
</div>
<div class="help-item">
<h4>VIP特权说明</h4>
<p>VIP会员享有更多功能权限优先处理专属客服等特权服务</p>
</div>
</div>
</van-action-sheet>
</div>
</template>
<script setup>
import { checkSession } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs'
import { showFailToast, showLoadingToast, showNotify } from 'vant'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLogin = ref(false)
const userInfo = ref({})
const packages = ref([])
const paymentMethods = ref([])
const selectedPackage = ref(null)
const selectedPayment = ref('alipay')
const payLoading = ref(false)
const showHelp = ref(false)
// VIP信息计算
const vipInfo = computed(() => {
const now = Date.now()
const expiredTime = userInfo.value.expired_time ? userInfo.value.expired_time * 1000 : 0
const isVip = expiredTime > now
const daysLeft = isVip ? Math.ceil((expiredTime - now) / (24 * 60 * 60 * 1000)) : 0
return { isVip, daysLeft }
})
// 会员特权配置
const privileges = ref([
{
key: 'unlimited',
title: '无限对话',
desc: '不限制对话次数',
icon: 'icon-chat',
color: '#07c160'
},
{
key: 'priority',
title: '优先处理',
desc: '请求优先处理',
icon: 'icon-flash',
color: '#ff9500'
},
{
key: 'models',
title: '高级模型',
desc: '使用最新AI模型',
icon: 'icon-star',
color: '#ffd700'
},
{
key: 'support',
title: '专属客服',
desc: '7×24小时客服支持',
icon: 'icon-service',
color: '#1989fa'
}
])
onMounted(() => {
initPage()
})
const initPage = async () => {
try {
const user = await checkSession()
isLogin.value = true
userInfo.value = user
// 获取用户详细信息
const profileRes = await httpGet('/api/user/profile')
userInfo.value = { ...userInfo.value, ...profileRes.data }
} catch (error) {
showLoginDialog(router)
return
}
// 获取充值套餐
fetchPackages()
// 获取支付方式
fetchPaymentMethods()
}
const fetchPackages = () => {
httpGet('/api/product/list')
.then((res) => {
// 添加推荐标签和特权描述
packages.value = res.data.map((pkg, index) => ({
...pkg,
recommended: index === 1, // 第二个套餐设为推荐
features: pkg.days > 30 ? 'VIP专属权益' : null
}))
})
.catch((e) => {
showFailToast('获取套餐失败:' + e.message)
})
}
const fetchPaymentMethods = () => {
httpGet('/api/payment/payWays')
.then((res) => {
paymentMethods.value = res.data.map(item => ({
type: item.pay_type,
name: item.pay_type === 'alipay' ? '支付宝' : '微信支付',
icon: item.pay_type === 'alipay' ? 'icon-alipay' : 'icon-wechat-pay',
color: item.pay_type === 'alipay' ? '#1677ff' : '#07c160',
payWay: item.pay_way
}))
if (paymentMethods.value.length > 0) {
selectedPayment.value = paymentMethods.value[0].type
}
})
.catch((e) => {
showFailToast('获取支付方式失败:' + e.message)
})
}
const selectPackage = (pkg) => {
selectedPackage.value = pkg
}
const processPay = () => {
if (!selectedPackage.value || !selectedPayment.value) {
showNotify({ type: 'warning', message: '请选择套餐和支付方式' })
return
}
const paymentMethod = paymentMethods.value.find(p => p.type === selectedPayment.value)
if (!paymentMethod) {
showNotify({ type: 'danger', message: '支付方式无效' })
return
}
payLoading.value = true
showLoadingToast({
message: '正在创建订单...',
forbidClick: true,
})
const host = `${location.protocol}//${location.host}`
httpPost('/api/payment/doPay', {
product_id: selectedPackage.value.id,
pay_way: paymentMethod.payWay,
pay_type: paymentMethod.type,
user_id: userInfo.value.id,
host: host,
device: 'mobile',
})
.then((res) => {
location.href = res.data
})
.catch((e) => {
payLoading.value = false
showFailToast('创建订单失败:' + e.message)
})
}
</script>
<style lang="scss" scoped>
.member-center {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.member-content {
padding: 54px 16px 20px;
.user-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
.user-info {
display: flex;
align-items: center;
margin-bottom: 16px;
.user-detail {
margin-left: 16px;
.user-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
}
}
.user-stats {
display: flex;
align-items: center;
justify-content: space-around;
.stat-item {
text-align: center;
.stat-value {
font-size: 20px;
font-weight: 700;
color: var(--van-primary-color);
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
}
.stat-divider {
width: 1px;
height: 30px;
background: #e5e5e5;
}
}
}
.section-title {
font-size: 18px;
font-weight: 600;
color: white;
margin: 0 0 16px 4px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.privileges-section {
margin-bottom: 24px;
.privileges-grid {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(10px);
.privilege-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&.active {
.privilege-icon {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.privilege-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
transition: all 0.3s ease;
.iconfont {
font-size: 20px;
color: white;
}
}
.privilege-info {
flex: 1;
.privilege-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.privilege-desc {
font-size: 13px;
color: #666;
}
}
.privilege-status {
margin-left: 8px;
}
}
}
}
.packages-section {
margin-bottom: 24px;
.packages-list {
.package-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
backdrop-filter: blur(10px);
position: relative;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
&:active {
transform: scale(0.98);
}
&.recommended {
border-color: var(--van-primary-color);
box-shadow: 0 8px 24px rgba(25, 137, 250, 0.3);
}
.package-tag {
position: absolute;
top: -1px;
right: 16px;
background: var(--van-primary-color);
color: white;
font-size: 12px;
padding: 4px 12px;
border-radius: 0 0 8px 8px;
}
.package-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.package-name {
font-size: 16px;
font-weight: 600;
color: #333;
}
.package-price {
text-align: right;
.current-price {
font-size: 18px;
font-weight: 700;
color: var(--van-primary-color);
}
.original-price {
font-size: 14px;
color: #999;
text-decoration: line-through;
margin-left: 8px;
}
}
}
.package-features {
.feature-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
color: #666;
&:last-child {
margin-bottom: 0;
}
.van-icon {
margin-right: 8px;
}
}
}
}
}
}
.payment-section {
margin-bottom: 24px;
:deep(.van-cell-group) {
border-radius: 12px;
overflow: hidden;
backdrop-filter: blur(10px);
.van-cell {
background: rgba(255, 255, 255, 0.95);
.payment-info {
display: flex;
align-items: center;
.iconfont {
font-size: 20px;
margin-right: 12px;
}
}
}
}
}
.pay-button {
position: sticky;
bottom: 20px;
z-index: 100;
}
}
.help-content {
padding: 20px;
.help-item {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
h4 {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: var(--van-gray-6);
line-height: 1.5;
margin: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,479 @@
<template>
<div class="power-log">
<van-nav-bar title="算力日志" left-arrow @click-left="router.back()" fixed>
<template #right>
<van-icon name="filter-o" @click="showFilter = true" />
</template>
</van-nav-bar>
<div class="power-content">
<!-- 统计概览 -->
<div class="stats-overview">
<div class="stats-card">
<van-row :gutter="12">
<van-col :span="8">
<div class="stat-item">
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">总消费</div>
</div>
</van-col>
<van-col :span="8">
<div class="stat-item">
<div class="stat-value">{{ stats.today }}</div>
<div class="stat-label">今日消费</div>
</div>
</van-col>
<van-col :span="8">
<div class="stat-item">
<div class="stat-value">{{ stats.balance }}</div>
<div class="stat-label">剩余算力</div>
</div>
</van-col>
</van-row>
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<van-tabs v-model:active="activeType" @change="onTypeChange" shrink>
<van-tab title="全部" name="all" />
<van-tab title="对话" name="chat" />
<van-tab title="绘画" name="image" />
<van-tab title="音乐" name="music" />
<van-tab title="视频" name="video" />
</van-tabs>
</div>
<!-- 日志列表 -->
<div class="log-list">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div v-for="item in logList" :key="item.id" class="log-item">
<div class="log-header">
<div class="log-icon" :style="{ backgroundColor: getTypeColor(item.type) }">
<i class="iconfont" :class="getTypeIcon(item.type)"></i>
</div>
<div class="log-info">
<div class="log-title">{{ item.title }}</div>
<div class="log-time">{{ formatTime(item.created_at) }}</div>
</div>
<div class="log-cost">
<span class="cost-value">-{{ item.cost }}</span>
<span class="cost-unit">算力</span>
</div>
</div>
<div class="log-detail" v-if="item.remark">
<van-text-ellipsis :content="item.remark" expand-text="展开" collapse-text="收起" />
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="!loading && logList.length === 0" description="暂无消费记录" />
</van-list>
</van-pull-refresh>
</div>
</div>
<!-- 筛选弹窗 -->
<van-action-sheet v-model:show="showFilter" title="筛选条件">
<div class="filter-content">
<van-form>
<van-field label="时间范围">
<template #input>
<van-button size="small" @click="showDatePicker = true">
{{ dateRange.start && dateRange.end ? `${dateRange.start} ${dateRange.end}` : '选择时间' }}
</van-button>
</template>
</van-field>
<van-field label="消费类型">
<template #input>
<van-radio-group v-model="filterType" direction="horizontal">
<van-radio name="all">全部</van-radio>
<van-radio name="chat">对话</van-radio>
<van-radio name="image">绘画</van-radio>
<van-radio name="music">音乐</van-radio>
</van-radio-group>
</template>
</van-field>
</van-form>
<div class="filter-actions">
<van-button @click="resetFilter">重置</van-button>
<van-button type="primary" @click="applyFilter">确定</van-button>
</div>
</div>
</van-action-sheet>
<!-- 日期选择器 -->
<van-calendar
v-model:show="showDatePicker"
type="range"
@confirm="onDateConfirm"
:max-date="new Date()"
/>
</div>
</template>
<script setup>
import { httpGet } from '@/utils/http'
import { showFailToast } from 'vant'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const logList = ref([])
const activeType = ref('all')
const showFilter = ref(false)
const showDatePicker = ref(false)
const filterType = ref('all')
const dateRange = ref({
start: '',
end: ''
})
// 统计数据
const stats = ref({
total: 0,
today: 0,
balance: 0
})
// 分页参数
const pageParams = ref({
page: 1,
limit: 20,
type: 'all'
})
onMounted(() => {
fetchStats()
onLoad()
})
// 获取统计数据
const fetchStats = () => {
// 这里应该调用实际的API
// httpGet('/api/user/power/stats').then(res => {
// stats.value = res.data
// })
// 临时使用模拟数据
stats.value = {
total: Math.floor(Math.random() * 10000),
today: Math.floor(Math.random() * 100),
balance: Math.floor(Math.random() * 1000)
}
}
// 加载日志列表
const onLoad = () => {
if (finished.value) return
loading.value = true
// 模拟API调用
setTimeout(() => {
const mockData = generateMockData(pageParams.value.page, pageParams.value.limit)
if (pageParams.value.page === 1) {
logList.value = mockData
} else {
logList.value.push(...mockData)
}
loading.value = false
pageParams.value.page++
// 模拟数据加载完成
if (pageParams.value.page > 5) {
finished.value = true
}
}, 1000)
}
// 下拉刷新
const onRefresh = () => {
finished.value = false
pageParams.value.page = 1
refreshing.value = true
setTimeout(() => {
logList.value = generateMockData(1, pageParams.value.limit)
refreshing.value = false
pageParams.value.page = 2
}, 1000)
}
// 类型切换
const onTypeChange = (type) => {
pageParams.value.type = type
pageParams.value.page = 1
finished.value = false
logList.value = []
onLoad()
}
// 生成模拟数据
const generateMockData = (page, limit) => {
const types = ['chat', 'image', 'music', 'video']
const titles = {
chat: ['GPT-4对话', 'Claude对话', '智能助手'],
image: ['MidJourney生成', 'Stable Diffusion', 'DALL-E创作'],
music: ['Suno音乐创作', '音频生成'],
video: ['视频生成', 'Luma创作']
}
const data = []
const startIndex = (page - 1) * limit
for (let i = 0; i < limit; i++) {
const id = startIndex + i + 1
const type = types[Math.floor(Math.random() * types.length)]
const title = titles[type][Math.floor(Math.random() * titles[type].length)]
// 如果有类型筛选且不匹配,跳过
if (pageParams.value.type !== 'all' && type !== pageParams.value.type) {
continue
}
data.push({
id,
type,
title,
cost: Math.floor(Math.random() * 50) + 1,
remark: Math.random() > 0.5 ? '消费详情使用高级模型进行AI创作效果优质' : '',
created_at: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString()
})
}
return data
}
// 获取类型图标
const getTypeIcon = (type) => {
const icons = {
chat: 'icon-chat',
image: 'icon-mj',
music: 'icon-music',
video: 'icon-video'
}
return icons[type] || 'icon-chat'
}
// 获取类型颜色
const getTypeColor = (type) => {
const colors = {
chat: '#1989fa',
image: '#8B5CF6',
music: '#ee0a24',
video: '#07c160'
}
return colors[type] || '#1989fa'
}
// 格式化时间
const formatTime = (timeStr) => {
const date = new Date(timeStr)
const now = new Date()
const diff = now - date
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 24小时内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return date.toLocaleDateString()
}
}
// 日期选择确认
const onDateConfirm = (values) => {
const [start, end] = values
dateRange.value = {
start: start.toLocaleDateString(),
end: end.toLocaleDateString()
}
showDatePicker.value = false
}
// 重置筛选
const resetFilter = () => {
filterType.value = 'all'
dateRange.value = { start: '', end: '' }
}
// 应用筛选
const applyFilter = () => {
activeType.value = filterType.value
pageParams.value.type = filterType.value
pageParams.value.page = 1
finished.value = false
logList.value = []
showFilter.value = false
onLoad()
}
</script>
<style lang="scss" scoped>
.power-log {
min-height: 100vh;
background: var(--van-background);
.power-content {
padding-top: 46px;
.stats-overview {
padding: 16px;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6);
.stats-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(10px);
.stat-item {
text-align: center;
.stat-value {
font-size: 20px;
font-weight: 700;
color: white;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
}
}
}
.filter-bar {
background: var(--van-background);
border-bottom: 1px solid var(--van-border-color);
:deep(.van-tabs__nav) {
padding: 0 16px;
}
:deep(.van-tab) {
font-size: 14px;
}
}
.log-list {
padding: 8px 16px 60px;
.log-item {
background: var(--van-cell-background);
border-radius: 12px;
margin-bottom: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.log-header {
display: flex;
align-items: center;
.log-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
.iconfont {
font-size: 20px;
color: white;
}
}
.log-info {
flex: 1;
.log-title {
font-size: 15px;
font-weight: 500;
color: var(--van-text-color);
margin-bottom: 4px;
}
.log-time {
font-size: 12px;
color: var(--van-gray-6);
}
}
.log-cost {
text-align: right;
.cost-value {
font-size: 16px;
font-weight: 600;
color: #ee0a24;
}
.cost-unit {
font-size: 12px;
color: var(--van-gray-6);
margin-left: 2px;
}
}
}
.log-detail {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--van-border-color);
:deep(.van-text-ellipsis__text) {
font-size: 13px;
color: var(--van-gray-6);
line-height: 1.4;
}
}
}
}
}
.filter-content {
padding: 20px;
.filter-actions {
display: flex;
gap: 12px;
margin-top: 20px;
.van-button {
flex: 1;
}
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.power-log {
.stats-card {
background: rgba(255, 255, 255, 0.05) !important;
}
.log-item {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,631 @@
<template>
<div class="settings-page">
<van-nav-bar title="设置" left-arrow @click-left="router.back()" fixed />
<div class="settings-content">
<!-- 个人设置 -->
<div class="setting-section">
<h3 class="section-title">个人设置</h3>
<van-cell-group inset>
<van-cell title="个人信息" is-link @click="router.push('/mobile/profile')">
<template #icon>
<i class="iconfont icon-user setting-icon"></i>
</template>
</van-cell>
<van-cell title="修改密码" is-link @click="showPasswordDialog = true">
<template #icon>
<i class="iconfont icon-lock setting-icon"></i>
</template>
</van-cell>
<van-cell title="绑定手机" is-link @click="showBindMobile = true">
<template #icon>
<i class="iconfont icon-phone setting-icon"></i>
</template>
</van-cell>
<van-cell title="绑定邮箱" is-link @click="showBindEmail = true">
<template #icon>
<i class="iconfont icon-email setting-icon"></i>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 应用设置 -->
<div class="setting-section">
<h3 class="section-title">应用设置</h3>
<van-cell-group inset>
<van-cell title="暗黑主题">
<template #icon>
<i class="iconfont icon-moon setting-icon"></i>
</template>
<template #right-icon>
<van-switch
v-model="darkMode"
@change="onThemeChange"
/>
</template>
</van-cell>
<van-cell title="流式输出">
<template #icon>
<i class="iconfont icon-stream setting-icon"></i>
</template>
<template #right-icon>
<van-switch
v-model="streamOutput"
@change="onStreamChange"
/>
</template>
</van-cell>
<van-cell title="消息通知">
<template #icon>
<i class="iconfont icon-bell setting-icon"></i>
</template>
<template #right-icon>
<van-switch v-model="notifications" />
</template>
</van-cell>
<van-cell title="自动保存">
<template #icon>
<i class="iconfont icon-save setting-icon"></i>
</template>
<template #right-icon>
<van-switch v-model="autoSave" />
</template>
</van-cell>
<van-cell title="语言设置" is-link @click="showLanguageSelect = true">
<template #icon>
<i class="iconfont icon-translate setting-icon"></i>
</template>
<template #value>
<span class="setting-value">{{ currentLanguage.name }}</span>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 聊天设置 -->
<div class="setting-section">
<h3 class="section-title">聊天设置</h3>
<van-cell-group inset>
<van-cell title="默认模型" is-link @click="showModelSelect = true">
<template #icon>
<i class="iconfont icon-robot setting-icon"></i>
</template>
<template #value>
<span class="setting-value">{{ currentModel.name }}</span>
</template>
</van-cell>
<van-cell title="对话记录">
<template #icon>
<i class="iconfont icon-history setting-icon"></i>
</template>
<template #right-icon>
<van-switch v-model="saveHistory" />
</template>
</van-cell>
<van-cell title="发送方式" is-link @click="showSendModeSelect = true">
<template #icon>
<i class="iconfont icon-send setting-icon"></i>
</template>
<template #value>
<span class="setting-value">{{ currentSendMode.name }}</span>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 隐私与安全 -->
<div class="setting-section">
<h3 class="section-title">隐私与安全</h3>
<van-cell-group inset>
<van-cell title="清除缓存" is-link @click="showClearCache = true">
<template #icon>
<i class="iconfont icon-delete setting-icon"></i>
</template>
<template #value>
<span class="setting-value">{{ cacheSize }}</span>
</template>
</van-cell>
<van-cell title="清除聊天记录" is-link @click="showClearHistory = true">
<template #icon>
<i class="iconfont icon-clear setting-icon"></i>
</template>
</van-cell>
<van-cell title="隐私政策" is-link @click="showPrivacyPolicy = true">
<template #icon>
<i class="iconfont icon-shield setting-icon"></i>
</template>
</van-cell>
<van-cell title="用户协议" is-link @click="showUserAgreement = true">
<template #icon>
<i class="iconfont icon-file setting-icon"></i>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 其他设置 -->
<div class="setting-section">
<h3 class="section-title">其他</h3>
<van-cell-group inset>
<van-cell title="检查更新" is-link @click="checkUpdate">
<template #icon>
<i class="iconfont icon-refresh setting-icon"></i>
</template>
<template #value>
<span class="setting-value">v{{ appVersion }}</span>
</template>
</van-cell>
<van-cell title="帮助中心" is-link @click="router.push('/mobile/help')">
<template #icon>
<i class="iconfont icon-help setting-icon"></i>
</template>
</van-cell>
<van-cell title="意见反馈" is-link @click="router.push('/mobile/feedback')">
<template #icon>
<i class="iconfont icon-message setting-icon"></i>
</template>
</van-cell>
<van-cell title="关于我们" is-link @click="showAbout = true">
<template #icon>
<i class="iconfont icon-info setting-icon"></i>
</template>
</van-cell>
</van-cell-group>
</div>
</div>
<!-- 修改密码弹窗 -->
<van-dialog
v-model:show="showPasswordDialog"
title="修改密码"
show-cancel-button
@confirm="updatePassword"
>
<van-form>
<van-cell-group inset>
<van-field
v-model="passwordForm.old"
type="password"
label="旧密码"
placeholder="请输入旧密码"
/>
<van-field
v-model="passwordForm.new"
type="password"
label="新密码"
placeholder="请输入新密码"
/>
<van-field
v-model="passwordForm.confirm"
type="password"
label="确认密码"
placeholder="请再次输入新密码"
/>
</van-cell-group>
</van-form>
</van-dialog>
<!-- 语言选择 -->
<van-action-sheet v-model:show="showLanguageSelect" title="选择语言">
<div class="language-options">
<van-cell
v-for="lang in languages"
:key="lang.code"
:title="lang.name"
clickable
@click="selectLanguage(lang)"
>
<template #right-icon>
<van-icon
v-if="currentLanguage.code === lang.code"
name="success"
color="#07c160"
/>
</template>
</van-cell>
</div>
</van-action-sheet>
<!-- 模型选择 -->
<van-action-sheet v-model:show="showModelSelect" title="选择默认模型">
<div class="model-options">
<van-cell
v-for="model in models"
:key="model.code"
:title="model.name"
:label="model.desc"
clickable
@click="selectModel(model)"
>
<template #right-icon>
<van-icon
v-if="currentModel.code === model.code"
name="success"
color="#07c160"
/>
</template>
</van-cell>
</div>
</van-action-sheet>
<!-- 发送方式选择 -->
<van-action-sheet v-model:show="showSendModeSelect" title="选择发送方式">
<div class="send-mode-options">
<van-cell
v-for="mode in sendModes"
:key="mode.code"
:title="mode.name"
:label="mode.desc"
clickable
@click="selectSendMode(mode)"
>
<template #right-icon>
<van-icon
v-if="currentSendMode.code === mode.code"
name="success"
color="#07c160"
/>
</template>
</van-cell>
</div>
</van-action-sheet>
<!-- 清除缓存确认 -->
<van-dialog
v-model:show="showClearCache"
title="清除缓存"
message="确定要清除所有缓存数据吗?这将删除临时文件和图片缓存。"
show-cancel-button
@confirm="clearCache"
/>
<!-- 清除聊天记录确认 -->
<van-dialog
v-model:show="showClearHistory"
title="清除聊天记录"
message="确定要清除所有聊天记录吗?此操作不可撤销。"
show-cancel-button
@confirm="clearHistory"
/>
<!-- 关于我们 -->
<van-dialog v-model:show="showAbout" title="关于我们" :show-cancel-button="false">
<div class="about-content">
<div class="about-logo">
<img src="/images/logo.png" alt="Logo" />
</div>
<h3>{{ appName }}</h3>
<p class="about-desc">
专业的AI创作平台提供对话绘画音乐视频等多种AI服务让创作更简单更高效
</p>
<div class="about-info">
<p>版本v{{ appVersion }}</p>
<p>更新时间2024-01-01</p>
</div>
</div>
</van-dialog>
</div>
</template>
<script setup>
import { useSharedStore } from '@/store/sharedata'
import { showNotify, showSuccessToast } from 'vant'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const store = useSharedStore()
// 基础状态
const appName = ref(import.meta.env.VITE_TITLE)
const appVersion = ref('2.1.0')
const cacheSize = ref('23.5MB')
// 设置状态
const darkMode = ref(store.theme === 'dark')
const streamOutput = ref(store.chatStream)
const notifications = ref(true)
const autoSave = ref(true)
const saveHistory = ref(true)
// 弹窗状态
const showPasswordDialog = ref(false)
const showBindMobile = ref(false)
const showBindEmail = ref(false)
const showLanguageSelect = ref(false)
const showModelSelect = ref(false)
const showSendModeSelect = ref(false)
const showClearCache = ref(false)
const showClearHistory = ref(false)
const showPrivacyPolicy = ref(false)
const showUserAgreement = ref(false)
const showAbout = ref(false)
// 表单数据
const passwordForm = ref({
old: '',
new: '',
confirm: ''
})
// 语言选项
const languages = ref([
{ code: 'zh-CN', name: '简体中文' },
{ code: 'zh-TW', name: '繁體中文' },
{ code: 'en', name: 'English' },
{ code: 'ja', name: '日本語' },
{ code: 'ko', name: '한국어' }
])
const currentLanguage = ref(languages.value[0])
// 模型选项
const models = ref([
{ code: 'gpt-4', name: 'GPT-4', desc: '最新的GPT-4模型性能强大' },
{ code: 'gpt-3.5', name: 'GPT-3.5', desc: '经典的GPT-3.5模型,速度快' },
{ code: 'claude', name: 'Claude', desc: '人工智能助手Claude' },
{ code: 'gemini', name: 'Gemini', desc: 'Google的Gemini模型' }
])
const currentModel = ref(models.value[0])
// 发送方式选项
const sendModes = ref([
{ code: 'enter', name: 'Enter发送', desc: '按Enter键发送消息' },
{ code: 'ctrl+enter', name: 'Ctrl+Enter发送', desc: '按Ctrl+Enter发送消息' },
{ code: 'button', name: '仅按钮发送', desc: '只能点击发送按钮' }
])
const currentSendMode = ref(sendModes.value[0])
onMounted(() => {
loadSettings()
})
// 加载设置
const loadSettings = () => {
// 从localStorage加载设置
const savedSettings = localStorage.getItem('app-settings')
if (savedSettings) {
const settings = JSON.parse(savedSettings)
darkMode.value = settings.darkMode ?? (store.theme === 'dark')
streamOutput.value = settings.streamOutput ?? store.chatStream
notifications.value = settings.notifications ?? true
autoSave.value = settings.autoSave ?? true
saveHistory.value = settings.saveHistory ?? true
// 恢复语言设置
const savedLang = languages.value.find(lang => lang.code === settings.language)
if (savedLang) {
currentLanguage.value = savedLang
}
// 恢复模型设置
const savedModel = models.value.find(model => model.code === settings.model)
if (savedModel) {
currentModel.value = savedModel
}
// 恢复发送方式设置
const savedSendMode = sendModes.value.find(mode => mode.code === settings.sendMode)
if (savedSendMode) {
currentSendMode.value = savedSendMode
}
}
}
// 保存设置
const saveSettings = () => {
const settings = {
darkMode: darkMode.value,
streamOutput: streamOutput.value,
notifications: notifications.value,
autoSave: autoSave.value,
saveHistory: saveHistory.value,
language: currentLanguage.value.code,
model: currentModel.value.code,
sendMode: currentSendMode.value.code
}
localStorage.setItem('app-settings', JSON.stringify(settings))
}
// 主题切换
const onThemeChange = (value) => {
store.setTheme(value ? 'dark' : 'light')
saveSettings()
}
// 流式输出切换
const onStreamChange = (value) => {
store.setChatStream(value)
saveSettings()
}
// 选择语言
const selectLanguage = (lang) => {
currentLanguage.value = lang
showLanguageSelect.value = false
saveSettings()
showSuccessToast(`已切换到${lang.name}`)
}
// 选择模型
const selectModel = (model) => {
currentModel.value = model
showModelSelect.value = false
saveSettings()
showSuccessToast(`已设置默认模型为${model.name}`)
}
// 选择发送方式
const selectSendMode = (mode) => {
currentSendMode.value = mode
showSendModeSelect.value = false
saveSettings()
showSuccessToast(`已设置发送方式为${mode.name}`)
}
// 修改密码
const updatePassword = () => {
if (!passwordForm.value.old) {
showNotify({ type: 'danger', message: '请输入旧密码' })
return
}
if (!passwordForm.value.new || passwordForm.value.new.length < 8) {
showNotify({ type: 'danger', message: '新密码长度不能少于8位' })
return
}
if (passwordForm.value.new !== passwordForm.value.confirm) {
showNotify({ type: 'danger', message: '两次输入的密码不一致' })
return
}
// 这里应该调用API
showSuccessToast('密码修改成功')
showPasswordDialog.value = false
passwordForm.value = { old: '', new: '', confirm: '' }
}
// 清除缓存
const clearCache = () => {
setTimeout(() => {
cacheSize.value = '0MB'
showSuccessToast('缓存清除成功')
}, 1000)
}
// 清除聊天记录
const clearHistory = () => {
// 这里应该调用API清除聊天记录
showSuccessToast('聊天记录清除成功')
}
// 检查更新
const checkUpdate = () => {
showNotify({ type: 'primary', message: '正在检查更新...' })
setTimeout(() => {
showNotify({ type: 'success', message: '当前已是最新版本' })
}, 2000)
}
</script>
<style lang="scss" scoped>
.settings-page {
min-height: 100vh;
background: var(--van-background);
.settings-content {
padding: 54px 16px 20px;
.setting-section {
margin-bottom: 24px;
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 16px 4px;
}
:deep(.van-cell-group) {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.van-cell {
padding: 16px;
.setting-icon {
font-size: 18px;
color: var(--van-primary-color);
margin-right: 12px;
}
.van-cell__title {
font-size: 15px;
font-weight: 500;
}
.setting-value {
font-size: 14px;
color: var(--van-gray-6);
}
}
}
}
}
.language-options,
.model-options,
.send-mode-options {
max-height: 400px;
overflow-y: auto;
:deep(.van-cell) {
padding: 16px 20px;
.van-cell__title {
font-size: 15px;
font-weight: 500;
}
.van-cell__label {
color: var(--van-gray-6);
font-size: 13px;
margin-top: 4px;
}
}
}
.about-content {
text-align: center;
padding: 20px;
.about-logo {
margin-bottom: 16px;
img {
width: 60px;
height: 60px;
border-radius: 12px;
}
}
h3 {
font-size: 20px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 12px 0;
}
.about-desc {
font-size: 14px;
color: var(--van-gray-6);
line-height: 1.5;
margin: 0 0 20px 0;
}
.about-info {
p {
font-size: 13px;
color: var(--van-gray-7);
margin: 0 0 4px 0;
&:last-child {
margin: 0;
}
}
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.settings-page {
.van-cell-group {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}
}
</style>

View File

@@ -0,0 +1,743 @@
<template>
<div class="tools-page">
<van-nav-bar title="AI 工具" left-arrow @click-left="router.back()" fixed />
<div class="tools-content">
<!-- 工具分类 -->
<van-tabs v-model:active="activeCategory" @change="onCategoryChange" sticky :offset-top="46">
<van-tab title="全部" name="all" />
<van-tab title="办公工具" name="office" />
<van-tab title="创意工具" name="creative" />
<van-tab title="学习工具" name="study" />
<van-tab title="生活工具" name="life" />
</van-tabs>
<!-- 工具列表 -->
<div class="tools-list">
<div
v-for="tool in filteredTools"
:key="tool.key"
class="tool-item"
@click="openTool(tool)"
>
<div class="tool-header">
<div class="tool-icon" :style="{ backgroundColor: tool.color }">
<i class="iconfont" :class="tool.icon"></i>
</div>
<div class="tool-info">
<div class="tool-name">{{ tool.name }}</div>
<div class="tool-desc">{{ tool.desc }}</div>
</div>
<div class="tool-status">
<van-tag :type="tool.status === 'available' ? 'success' : 'warning'" size="medium">
{{ tool.status === 'available' ? '可用' : '开发中' }}
</van-tag>
</div>
</div>
<div class="tool-features" v-if="tool.features">
<van-tag
v-for="feature in tool.features"
:key="feature"
size="small"
plain
class="feature-tag"
>
{{ feature }}
</van-tag>
</div>
<div class="tool-stats" v-if="tool.stats">
<div class="stat-item">
<span class="stat-label">使用次数</span>
<span class="stat-value">{{ tool.stats.usageCount }}</span>
</div>
<div class="stat-item">
<span class="stat-label">好评率</span>
<span class="stat-value">{{ tool.stats.rating }}%</span>
</div>
</div>
</div>
<!-- 空状态 -->
<van-empty v-if="filteredTools.length === 0" description="该分类暂无工具" />
</div>
<!-- 推荐工具 -->
<div class="recommend-section" v-if="activeCategory === 'all'">
<h3 class="section-title">推荐工具</h3>
<van-swipe :autoplay="3000" class="recommend-swipe">
<van-swipe-item v-for="tool in recommendTools" :key="tool.key">
<div class="recommend-card" @click="openTool(tool)">
<div class="recommend-bg" :style="{ backgroundColor: tool.color }">
<i class="iconfont" :class="tool.icon"></i>
</div>
<div class="recommend-content">
<h4 class="recommend-title">{{ tool.name }}</h4>
<p class="recommend-desc">{{ tool.desc }}</p>
<van-button size="small" type="primary" plain round>
立即使用
</van-button>
</div>
</div>
</van-swipe-item>
</van-swipe>
</div>
</div>
<!-- 工具详情弹窗 -->
<van-action-sheet v-model:show="showToolDetail" :title="selectedTool?.name">
<div class="tool-detail" v-if="selectedTool">
<div class="detail-header">
<div class="detail-icon" :style="{ backgroundColor: selectedTool.color }">
<i class="iconfont" :class="selectedTool.icon"></i>
</div>
<div class="detail-info">
<h3 class="detail-name">{{ selectedTool.name }}</h3>
<p class="detail-desc">{{ selectedTool.fullDesc || selectedTool.desc }}</p>
</div>
</div>
<div class="detail-features" v-if="selectedTool.detailFeatures">
<h4 class="features-title">功能特点</h4>
<ul class="features-list">
<li v-for="feature in selectedTool.detailFeatures" :key="feature">
<van-icon name="checked" color="#07c160" />
{{ feature }}
</li>
</ul>
</div>
<div class="detail-usage" v-if="selectedTool.usage">
<h4 class="usage-title">使用说明</h4>
<p class="usage-text">{{ selectedTool.usage }}</p>
</div>
<div class="detail-actions">
<van-button
type="primary"
size="large"
round
block
:disabled="selectedTool.status !== 'available'"
@click="useTool(selectedTool)"
>
{{ selectedTool.status === 'available' ? '开始使用' : '开发中' }}
</van-button>
</div>
</div>
</van-action-sheet>
<!-- 思维导图工具 -->
<van-action-sheet v-model:show="showMindMap" title="思维导图" :close-on-click-overlay="false">
<div class="mindmap-container">
<div class="mindmap-toolbar">
<van-button size="small" @click="createNewMap">新建</van-button>
<van-button size="small" @click="saveMap">保存</van-button>
<van-button size="small" @click="exportMap">导出</van-button>
<van-button size="small" @click="closeMindMap">关闭</van-button>
</div>
<div class="mindmap-canvas" ref="mindmapCanvas">
<!-- 这里会渲染思维导图 -->
<div class="canvas-placeholder">
<i class="iconfont icon-mind"></i>
<p>思维导图工具</p>
<p class="placeholder-desc">功能开发中敬请期待</p>
</div>
</div>
</div>
</van-action-sheet>
</div>
</template>
<script setup>
import { showNotify } from 'vant'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const activeCategory = ref('all')
const selectedTool = ref(null)
const showToolDetail = ref(false)
const showMindMap = ref(false)
const mindmapCanvas = ref()
// 工具列表配置
const tools = ref([
{
key: 'mindmap',
name: '思维导图',
desc: '智能生成思维导图,整理思路更清晰',
fullDesc: '基于AI技术的智能思维导图生成工具可以根据文本内容自动生成结构化的思维导图支持多种导出格式。',
icon: 'icon-mind',
color: '#3B82F6',
category: 'office',
status: 'available',
features: ['自动生成', '多种模板', '智能布局'],
detailFeatures: [
'支持文本自动转思维导图',
'提供多种精美模板',
'智能节点布局算法',
'支持导出多种格式',
'支持在线协作编辑'
],
usage: '输入您的文本内容AI会自动分析并生成对应的思维导图结构。您可以对生成的导图进行编辑、美化和导出。',
stats: {
usageCount: 1256,
rating: 96
}
},
{
key: 'summary',
name: '文档总结',
desc: '快速提取文档要点,生成精准摘要',
fullDesc: '智能文档总结工具,能够快速分析长文档并提取关键信息,生成简洁明了的摘要。',
icon: 'icon-doc',
color: '#10B981',
category: 'office',
status: 'available',
features: ['关键词提取', '智能摘要', '多语言支持'],
detailFeatures: [
'支持多种文档格式',
'智能关键词提取',
'可控制摘要长度',
'支持批量处理',
'多语言文档支持'
],
usage: '上传或粘贴文档内容选择摘要长度和类型AI会自动生成文档摘要。',
stats: {
usageCount: 2341,
rating: 94
}
},
{
key: 'translation',
name: '智能翻译',
desc: '高质量多语言翻译,支持专业术语',
fullDesc: '基于先进AI模型的多语言翻译工具支持100+语言互译,特别适合专业文档翻译。',
icon: 'icon-translate',
color: '#8B5CF6',
category: 'office',
status: 'available',
features: ['100+语言', '专业术语', '上下文理解'],
detailFeatures: [
'支持100多种语言互译',
'专业术语库支持',
'上下文语境理解',
'批量文档翻译',
'翻译质量评估'
],
usage: '选择源语言和目标语言输入需要翻译的内容AI会提供高质量的翻译结果。',
stats: {
usageCount: 5678,
rating: 98
}
},
{
key: 'poster',
name: '海报设计',
desc: '一键生成专业海报,多种风格可选',
fullDesc: 'AI驱动的海报设计工具提供丰富的模板和素材轻松制作专业级海报。',
icon: 'icon-design',
color: '#F59E0B',
category: 'creative',
status: 'available',
features: ['模板丰富', '一键生成', '高清输出'],
detailFeatures: [
'500+精美模板',
'智能配色方案',
'自动排版布局',
'高清无水印导出',
'支持自定义尺寸'
],
usage: '选择海报类型和风格输入文案内容AI会自动生成专业海报设计。',
stats: {
usageCount: 3456,
rating: 95
}
},
{
key: 'logo',
name: 'Logo 设计',
desc: 'AI 生成独特Logo商用级品质',
fullDesc: '专业的AI Logo设计工具根据您的品牌理念生成独特的Logo设计方案。',
icon: 'icon-logo',
color: '#EF4444',
category: 'creative',
status: 'available',
features: ['品牌风格', '矢量格式', '商用授权'],
detailFeatures: [
'多种设计风格选择',
'矢量格式输出',
'商用版权授权',
'配色方案推荐',
'标准化尺寸规范'
],
usage: '描述您的品牌特点和期望风格AI会生成多个Logo设计方案供您选择。',
stats: {
usageCount: 2234,
rating: 93
}
},
{
key: 'study-plan',
name: '学习计划',
desc: '个性化学习路径规划,提升学习效率',
fullDesc: '基于AI的个性化学习计划制定工具根据您的学习目标和时间安排制定最优学习路径。',
icon: 'icon-study',
color: '#06B6D4',
category: 'study',
status: 'available',
features: ['个性化', '进度跟踪', '智能调整'],
detailFeatures: [
'个性化学习路径',
'学习进度跟踪',
'智能计划调整',
'学习效果评估',
'多领域知识覆盖'
],
usage: '输入您的学习目标、可用时间和当前水平AI会为您制定详细的学习计划。',
stats: {
usageCount: 1890,
rating: 97
}
},
{
key: 'recipe',
name: '智能食谱',
desc: '根据食材推荐美食,营养搭配建议',
fullDesc: '智能食谱推荐系统,根据现有食材推荐美食制作方法,提供营养搭配建议。',
icon: 'icon-food',
color: '#F97316',
category: 'life',
status: 'development',
features: ['食材识别', '营养分析', '制作指导'],
detailFeatures: [
'食材智能识别',
'营养成分分析',
'详细制作步骤',
'口味偏好适配',
'热量控制建议'
],
usage: '拍照或输入现有食材AI会推荐适合的菜谱并提供详细制作指导。',
stats: {
usageCount: 567,
rating: 89
}
},
{
key: 'workout',
name: '运动计划',
desc: '定制化健身方案,科学训练指导',
fullDesc: '个性化运动健身计划制定工具,根据身体状况和目标制定科学的训练方案。',
icon: 'icon-sport',
color: '#EC4899',
category: 'life',
status: 'development',
features: ['个性定制', '科学指导', '进度跟踪'],
detailFeatures: [
'个性化训练计划',
'科学运动指导',
'训练进度跟踪',
'饮食建议搭配',
'健康数据分析'
],
usage: '输入您的身体状况、运动目标和时间安排AI会制定适合的运动计划。',
stats: {
usageCount: 234,
rating: 91
}
}
])
// 推荐工具取前3个可用的
const recommendTools = computed(() => {
return tools.value.filter(tool => tool.status === 'available').slice(0, 3)
})
// 根据分类筛选工具
const filteredTools = computed(() => {
if (activeCategory.value === 'all') {
return tools.value
}
return tools.value.filter(tool => tool.category === activeCategory.value)
})
onMounted(() => {
// 检查URL参数如果有指定工具则直接打开
const urlParams = new URLSearchParams(window.location.search)
const toolKey = urlParams.get('tool')
if (toolKey) {
const tool = tools.value.find(t => t.key === toolKey)
if (tool) {
openTool(tool)
}
}
})
// 分类切换
const onCategoryChange = (category) => {
// 可以在这里添加数据加载逻辑
}
// 打开工具
const openTool = (tool) => {
selectedTool.value = tool
// 特殊工具直接打开对应界面
if (tool.key === 'mindmap') {
showMindMap.value = true
} else {
showToolDetail.value = true
}
}
// 使用工具
const useTool = (tool) => {
showToolDetail.value = false
if (tool.status !== 'available') {
showNotify({ type: 'warning', message: '该工具还在开发中,敬请期待' })
return
}
// 根据工具类型跳转到对应页面或打开功能界面
switch (tool.key) {
case 'mindmap':
showMindMap.value = true
break
case 'summary':
case 'translation':
case 'poster':
case 'logo':
case 'study-plan':
showNotify({ type: 'primary', message: `正在启动${tool.name}工具...` })
// 这里可以跳转到具体的工具页面
break
default:
showNotify({ type: 'warning', message: '功能开发中' })
}
}
// 思维导图相关方法
const createNewMap = () => {
showNotify({ type: 'primary', message: '创建新的思维导图' })
}
const saveMap = () => {
showNotify({ type: 'success', message: '思维导图已保存' })
}
const exportMap = () => {
showNotify({ type: 'primary', message: '导出思维导图' })
}
const closeMindMap = () => {
showMindMap.value = false
}
</script>
<style lang="scss" scoped>
.tools-page {
min-height: 100vh;
background: var(--van-background);
.tools-content {
padding-top: 46px;
:deep(.van-tabs__nav) {
background: var(--van-background);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.tools-list {
padding: 16px;
.tool-item {
background: var(--van-cell-background);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.tool-header {
display: flex;
align-items: center;
margin-bottom: 12px;
.tool-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
.iconfont {
font-size: 22px;
color: white;
}
}
.tool-info {
flex: 1;
.tool-name {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
margin-bottom: 4px;
}
.tool-desc {
font-size: 13px;
color: var(--van-gray-6);
line-height: 1.4;
}
}
.tool-status {
margin-left: 8px;
}
}
.tool-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
.feature-tag {
font-size: 11px;
}
}
.tool-stats {
display: flex;
gap: 16px;
font-size: 12px;
color: var(--van-gray-6);
.stat-label {
margin-right: 4px;
}
.stat-value {
color: var(--van-text-color);
font-weight: 500;
}
}
}
}
.recommend-section {
padding: 0 16px 16px;
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 16px 0;
}
.recommend-swipe {
height: 160px;
border-radius: 12px;
overflow: hidden;
.recommend-card {
height: 100%;
position: relative;
display: flex;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6);
cursor: pointer;
.recommend-bg {
position: absolute;
top: 16px;
right: 16px;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
.iconfont {
font-size: 40px;
color: rgba(255, 255, 255, 0.8);
}
}
.recommend-content {
flex: 1;
color: white;
.recommend-title {
font-size: 20px;
font-weight: 700;
margin: 0 0 8px 0;
}
.recommend-desc {
font-size: 14px;
opacity: 0.9;
margin: 0 0 16px 0;
line-height: 1.4;
}
}
}
}
}
}
.tool-detail {
padding: 20px;
.detail-header {
display: flex;
margin-bottom: 20px;
.detail-icon {
width: 60px;
height: 60px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
flex-shrink: 0;
.iconfont {
font-size: 28px;
color: white;
}
}
.detail-info {
flex: 1;
.detail-name {
font-size: 20px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 8px 0;
}
.detail-desc {
font-size: 14px;
color: var(--van-gray-6);
line-height: 1.5;
margin: 0;
}
}
}
.detail-features,
.detail-usage {
margin-bottom: 20px;
.features-title,
.usage-title {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 12px 0;
}
.features-list {
padding: 0;
margin: 0;
list-style: none;
li {
display: flex;
align-items: center;
font-size: 14px;
color: var(--van-text-color);
margin-bottom: 8px;
.van-icon {
margin-right: 8px;
}
}
}
.usage-text {
font-size: 14px;
color: var(--van-gray-6);
line-height: 1.5;
margin: 0;
}
}
.detail-actions {
margin-top: 20px;
}
}
.mindmap-container {
height: 80vh;
display: flex;
flex-direction: column;
.mindmap-toolbar {
display: flex;
gap: 8px;
padding: 12px 16px;
background: var(--van-background-2);
border-bottom: 1px solid var(--van-border-color);
}
.mindmap-canvas {
flex: 1;
position: relative;
background: var(--van-background);
.canvas-placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--van-gray-6);
.iconfont {
font-size: 48px;
margin-bottom: 16px;
color: var(--van-gray-5);
}
p {
margin: 0 0 8px 0;
font-size: 16px;
&.placeholder-desc {
font-size: 14px;
opacity: 0.8;
}
}
}
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.tools-page {
.tool-item {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}
}
</style>