mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-23 03:24:34 +08:00
移动端重构第一版
This commit is contained in:
138
web/src/main.js
138
web/src/main.js
@@ -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')
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
169
web/src/views/mobile/Create.vue
Normal file
169
web/src/views/mobile/Create.vue
Normal 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>
|
||||
328
web/src/views/mobile/Discover.vue
Normal file
328
web/src/views/mobile/Discover.vue
Normal 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>
|
||||
177
web/src/views/mobile/Feedback.vue
Normal file
177
web/src/views/mobile/Feedback.vue
Normal 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>
|
||||
846
web/src/views/mobile/Help.vue
Normal file
846
web/src/views/mobile/Help.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 黑色主题
|
||||
|
||||
@@ -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>
|
||||
|
||||
739
web/src/views/mobile/Invite.vue
Normal file
739
web/src/views/mobile/Invite.vue
Normal 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>
|
||||
608
web/src/views/mobile/Member.vue
Normal file
608
web/src/views/mobile/Member.vue
Normal 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>
|
||||
479
web/src/views/mobile/PowerLog.vue
Normal file
479
web/src/views/mobile/PowerLog.vue
Normal 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
631
web/src/views/mobile/Settings.vue
Normal file
631
web/src/views/mobile/Settings.vue
Normal 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>
|
||||
743
web/src/views/mobile/Tools.vue
Normal file
743
web/src/views/mobile/Tools.vue
Normal 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>
|
||||
Reference in New Issue
Block a user