调整移动端页面UI布局

This commit is contained in:
GeekMaster
2025-08-04 12:08:42 +08:00
parent f7cf992598
commit e994060e93
28 changed files with 1393 additions and 1686 deletions

View File

@@ -1,5 +1,13 @@
# 更新日志 # 更新日志
## v4.2.6
- 功能优化:优化移动端首页 UI增加返回首页按钮
- 功能优化:优化移动端聊天页面 UI增加返回首页按钮
- 功能优化:优化移动端登录页面 UI增加返回首页按钮
- 功能优化:优化移动端注册页面 UI增加返回首页按钮
- 功能优化:优化移动端找回密码页面 UI增加返回首页按钮
## v4.2.5 ## v4.2.5
- 功能优化:在代码右下角增加复制代码功能按钮,增加收起和展开代码功能 - 功能优化:在代码右下角增加复制代码功能按钮,增加收起和展开代码功能

View File

@@ -2,9 +2,6 @@
.mobile-chat-list { .mobile-chat-list {
.content { .content {
padding-top: 46px;
padding-bottom: 60px;
.van-list { .van-list {
.van-cell__value { .van-cell__value {
.chat-list-item { .chat-list-item {

View File

@@ -2,15 +2,13 @@
<div class="foot-container"> <div class="foot-container">
<div class="footer"> <div class="footer">
<div> <div>
<span>{{ copyRight }}</span>
</div>
<div v-if="!license?.de_copy">
<a :href="gitURL" target="_blank"> <a :href="gitURL" target="_blank">
{{ title }} - {{ title }} -
{{ version }} {{ version }}
</a> </a>
</div> </div>
<div v-if="icp"> <div>
<span class="mr-2">{{ copyRight }}</span>
<a href="https://beian.miit.gov.cn" target="_blank">{{ icp }}</a> <a href="https://beian.miit.gov.cn" target="_blank">{{ icp }}</a>
</div> </div>
</div> </div>
@@ -70,7 +68,7 @@ getLicenseInfo()
margin-top: -4px; margin-top: -4px;
.footer { .footer {
max-width: 400px; // max-width: 400px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
padding: 20px; padding: 20px;

View File

@@ -0,0 +1,45 @@
<template>
<div v-if="active" class="custom-tab-pane">
<slot></slot>
</div>
</template>
<script setup>
import { computed, inject, useSlots } from 'vue'
const props = defineProps({
label: {
type: String,
required: false,
},
name: {
type: String,
required: true,
},
})
const slots = useSlots()
// 从父组件注入当前激活的 tab
const currentTab = inject('currentTab', '')
const active = computed(() => {
return currentTab.value === props.name
})
// 向父组件提供当前 pane 的信息,优先使用 labelSlot
const parentRegisterPane = inject('registerPane', () => {})
// 立即注册,不要等到 onMounted
parentRegisterPane({
name: props.name,
label: props.label || '', // 如果没有传 label 则使用空字符串
labelSlot: slots.label,
})
</script>
<style scoped>
.custom-tab-pane {
width: 100%;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<div class="w-full">
<div class="relative bg-gray-100 rounded-lg py-1 mb-2.5 overflow-hidden" ref="tabsHeader">
<div class="flex whitespace-nowrap overflow-x-auto scrollbar-hide" ref="tabsContainer">
<div
class="flex-shrink-0 text-center py-1.5 px-3 font-medium text-gray-700 cursor-pointer transition-colors duration-300 rounded-md relative z-20 hover:text-purple-600"
v-for="(tab, index) in panes"
:key="tab.name"
:class="{ '!text-purple-600': modelValue === tab.name }"
@click="handleTabClick(tab.name, index)"
ref="tabItems"
>
<component v-if="tab.labelSlot" :is="{ render: () => tab.labelSlot() }" />
<template v-else>
{{ tab.label }}
</template>
</div>
</div>
<div
class="absolute top-1 bottom-1 bg-white rounded-md shadow-sm transition-all duration-300 ease-out z-10"
:style="indicatorStyle"
ref="indicator"
></div>
</div>
<div>
<slot></slot>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onMounted, provide, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
required: true,
},
})
const emit = defineEmits(['update:modelValue', 'tab-click'])
const tabsHeader = ref(null)
const tabsContainer = ref(null)
const tabItems = ref([])
const indicator = ref(null)
const panes = ref([])
const indicatorStyle = ref({
transform: 'translateX(0px)',
width: '0px',
})
// 提供当前激活的 tab 给子组件
provide(
'currentTab',
computed(() => props.modelValue)
)
// 提供注册 pane 的方法给子组件
provide('registerPane', (pane) => {
// 检查是否已经存在相同 name 的 pane避免重复注册
const existingIndex = panes.value.findIndex((p) => p.name === pane.name)
if (existingIndex === -1) {
// 不存在则添加
panes.value.push(pane)
} else {
// 存在则更新
panes.value[existingIndex] = pane
}
})
const handleTabClick = (tabName, index) => {
emit('update:modelValue', tabName)
emit('tab-click', tabName, index)
updateIndicator(index)
}
const updateIndicator = async (activeIndex) => {
await nextTick()
if (tabItems.value && tabItems.value.length > 0 && tabsHeader.value) {
const activeTab = tabItems.value[activeIndex]
if (activeTab) {
const tabRect = activeTab.getBoundingClientRect()
const containerRect = tabsHeader.value.getBoundingClientRect()
const leftPosition = tabRect.left - containerRect.left
const tabWidth = tabRect.width
indicatorStyle.value = {
transform: `translateX(${leftPosition}px)`,
width: `${tabWidth}px`,
}
}
}
}
// 监听 modelValue 变化,更新指示器位置
watch(
() => props.modelValue,
(newValue) => {
const activeIndex = panes.value.findIndex((pane) => pane.name === newValue)
if (activeIndex !== -1) {
updateIndicator(activeIndex)
}
}
)
onMounted(() => {
// 初始化指示器位置
nextTick(() => {
const activeIndex = panes.value.findIndex((pane) => pane.name === props.modelValue)
if (activeIndex !== -1) {
updateIndicator(activeIndex)
}
})
})
</script>
<style scoped>
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
/* 确保标签页容器有足够的内边距 */
.overflow-x-auto {
padding: 0 4px;
}
/* 优化标签页间距 */
.flex-shrink-0 {
margin-right: 4px;
}
.flex-shrink-0:last-child {
margin-right: 0;
}
</style>

View File

@@ -59,7 +59,9 @@ import {
ShareSheet, ShareSheet,
Slider, Slider,
Sticky, Sticky,
Swipe,
SwipeCell, SwipeCell,
SwipeItem,
Switch, Switch,
Tab, Tab,
Tabbar, Tabbar,
@@ -101,6 +103,8 @@ app.use(DropdownMenu)
app.use(Icon) app.use(Icon)
app.use(DropdownItem) app.use(DropdownItem)
app.use(Sticky) app.use(Sticky)
app.use(Swipe)
app.use(SwipeItem)
app.use(SwipeCell) app.use(SwipeCell)
app.use(Dialog) app.use(Dialog)
app.use(ShareSheet) app.use(ShareSheet)

View File

@@ -123,26 +123,12 @@ const routes = [
meta: { title: '导出会话记录' }, meta: { title: '导出会话记录' },
component: () => import('@/views/ChatExport.vue'), component: () => import('@/views/ChatExport.vue'),
}, },
{
name: 'login',
path: '/login',
meta: { title: '用户登录' },
component: () => import('@/views/Login.vue'),
},
{ {
name: 'login-callback', name: 'login-callback',
path: '/login/callback', path: '/login/callback',
meta: { title: '用户登录' }, meta: { title: '用户登录' },
component: () => import('@/views/LoginCallback.vue'), component: () => import('@/views/LoginCallback.vue'),
}, },
{
name: 'register',
path: '/register',
meta: { title: '用户注册' },
component: () => import('@/views/Register.vue'),
},
{ {
name: 'resetpassword', name: 'resetpassword',
path: '/resetpassword', path: '/resetpassword',

View File

@@ -8,7 +8,7 @@
<img :src="logo" class="logo" alt="Geek-AI" /> <img :src="logo" class="logo" alt="Geek-AI" />
</div> </div>
<div class="menu-item"> <div class="menu-item">
<span v-if="!license?.de_copy"> <span v-if="!license || !license.de_copy">
<el-tooltip class="box-item" content="部署文档" placement="bottom"> <el-tooltip class="box-item" content="部署文档" placement="bottom">
<a :href="docsURL" class="link-button mr-3" target="_blank"> <a :href="docsURL" class="link-button mr-3" target="_blank">
<i class="iconfont icon-book"></i> <i class="iconfont icon-book"></i>
@@ -28,7 +28,7 @@
<span v-if="!isLogin"> <span v-if="!isLogin">
<el-button <el-button
@click="router.push('/login')" @click="showLoginDialog = true"
class="btn-go animate__animated animate__pulse animate__infinite" class="btn-go animate__animated animate__pulse animate__infinite"
round round
>登录/注册</el-button >登录/注册</el-button
@@ -79,6 +79,18 @@
<footer-bar /> <footer-bar />
<!-- 登录弹窗 -->
<el-dialog v-model="showLoginDialog" width="500px" @close="showLoginDialog = false">
<template #header>
<div class="text-center text-xl" style="color: var(--theme-text-color-primary)">
登录解锁更多功能
</div>
</template>
<div class="p-4 pt-2 pb-2">
<LoginDialog @success="loginSuccess" @hide="showLoginDialog = false" />
</div>
</el-dialog>
<!-- 网站公告对话框 --> <!-- 网站公告对话框 -->
<el-dialog v-model="showNotice" :show-close="true" class="notice-dialog" title="网站公告"> <el-dialog v-model="showNotice" :show-close="true" class="notice-dialog" title="网站公告">
<div class="notice"> <div class="notice">
@@ -96,6 +108,7 @@
<script setup> <script setup>
import FooterBar from '@/components/FooterBar.vue' import FooterBar from '@/components/FooterBar.vue'
import LoginDialog from '@/components/LoginDialog.vue'
import ThemeChange from '@/components/ThemeChange.vue' import ThemeChange from '@/components/ThemeChange.vue'
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache' import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
import { removeUserToken } from '@/store/session' import { removeUserToken } from '@/store/session'
@@ -113,6 +126,7 @@ const logo = ref('')
const license = ref({ de_copy: true }) const license = ref({ de_copy: true })
const isLogin = ref(false) const isLogin = ref(false)
const showLoginDialog = ref(false)
const docsURL = ref(import.meta.env.VITE_DOCS_URL) const docsURL = ref(import.meta.env.VITE_DOCS_URL)
const githubURL = ref(import.meta.env.VITE_GITHUB_URL) const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
const giteeURL = ref(import.meta.env.VITE_GITEE_URL) const giteeURL = ref(import.meta.env.VITE_GITEE_URL)
@@ -185,7 +199,12 @@ onMounted(() => {
const logout = () => { const logout = () => {
removeUserToken() removeUserToken()
router.push('/login') isLogin.value = false
}
const loginSuccess = () => {
isLogin.value = true
showLoginDialog.value = false
} }
// 不再显示公告 // 不再显示公告

View File

@@ -1,377 +0,0 @@
<template>
<div class="flex-center loginPage">
<div class="left">
<div class="login-box">
<AccountTop>
<template #default>
<div class="wechatLog flex-center" v-if="wechatLoginURL !== ''">
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)">
<i class="iconfont icon-wechat"></i>使用微信登录
</a>
</div>
</template>
</AccountTop>
<div class="input-form">
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules">
<el-form-item label="" prop="username">
<div class="form-title">账号</div>
<el-input
v-model="ruleForm.username"
size="large"
placeholder="请输入账号"
@keyup="handleKeyup"
/>
</el-form-item>
<el-form-item label="" prop="password">
<div class="flex-between w100">
<div class="form-title">密码</div>
<div class="form-forget text-color-primary" @click="router.push('/resetpassword')">
忘记密码
</div>
</div>
<el-input
size="large"
v-model="ruleForm.password"
placeholder="请输入密码"
show-password
autocomplete="off"
@keyup="handleKeyup"
/>
</el-form-item>
<el-form-item label="" prop="agreement" :class="{ 'agreement-error': agreementError }">
<div class="agreement-box" :class="{ shake: isShaking }">
<el-checkbox v-model="ruleForm.agreement" @change="handleAgreementChange">
我已阅读并同意
<span class="agreement-link" @click.stop.prevent="openAgreement"
>用户协议</span
>
<span class="agreement-link" @click.stop.prevent="openPrivacy">隐私政策</span>
</el-checkbox>
</div>
</el-form-item>
<el-form-item>
<el-button class="login-btn" size="large" type="primary" @click="login"
>登录</el-button
>
</el-form-item>
</el-form>
</div>
</div>
</div>
<account-bg />
<captcha v-if="enableVerify" @success="doLogin" ref="captchaRef" />
</div>
</template>
<script setup>
import AccountBg from '@/components/AccountBg.vue'
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
import { setUserToken } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import { setRoute } from '@/store/system'
import { showMessageError } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessageBox } from 'element-plus'
import MarkdownIt from 'markdown-it'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import AccountTop from '@/components/AccountTop.vue'
import Captcha from '@/components/Captcha.vue'
const router = useRouter()
const title = ref('')
const logo = ref('')
const licenseConfig = ref({})
const wechatLoginURL = ref('')
const enableVerify = ref(false)
const captchaRef = ref(null)
const ruleFormRef = ref(null)
const ruleForm = reactive({
username: import.meta.env.VITE_USER,
password: import.meta.env.VITE_PASS,
agreement: false,
})
const rules = {
username: [{ required: true, trigger: 'blur', message: '请输入账号' }],
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
agreement: [{ required: true, trigger: 'change', message: '请同意用户协议' }],
}
const agreementContent = ref('')
const privacyContent = ref('')
// 初始化markdown解析器
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
})
onMounted(() => {
// 检查URL中是否存在token参数
const urlParams = new URLSearchParams(window.location.search)
const token = urlParams.get('token')
if (token) {
setUserToken(token)
store.setIsLogin(true)
router.push('/chat')
return
}
// 获取系统配置
getSystemInfo()
.then((res) => {
logo.value = res.data.logo
title.value = res.data.title
enableVerify.value = res.data['enabled_verify']
})
.catch((e) => {
showMessageError('获取系统配置失败:' + e.message)
title.value = 'Geek-AI'
})
// 获取用户协议
httpGet('/api/config/get?key=agreement')
.then((res) => {
if (res.data && res.data.content) {
agreementContent.value = res.data.content
} else {
agreementContent.value =
'用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'
}
})
.catch((e) => {
agreementContent.value =
'用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'
})
// 获取隐私政策
httpGet('/api/config/get?key=privacy')
.then((res) => {
if (res.data && res.data.content) {
privacyContent.value = res.data.content
} else {
privacyContent.value =
'我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
}
})
.catch((e) => {
privacyContent.value =
'我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
})
getLicenseInfo()
.then((res) => {
licenseConfig.value = res.data
})
.catch((e) => {
showMessageError('获取 License 配置:' + e.message)
})
checkSession()
.then(() => {
router.back()
})
.catch(() => {})
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
httpGet('/api/user/clogin?return_url=' + returnURL)
.then((res) => {
wechatLoginURL.value = res.data.url
})
.catch((e) => {
console.error(e)
})
})
const handleKeyup = (e) => {
if (e.key === 'Enter') {
login()
}
}
const login = async function () {
if (!ruleForm.agreement) {
agreementError.value = true
isShaking.value = true
setTimeout(() => {
isShaking.value = false
}, 500)
showMessageError('请先阅读并同意用户协议')
return
}
await ruleFormRef.value.validate(async (valid) => {
if (valid) {
if (enableVerify.value) {
captchaRef.value.loadCaptcha()
} else {
doLogin({})
}
}
})
}
const store = useSharedStore()
const doLogin = (verifyData) => {
httpPost('/api/user/login', {
username: ruleForm.username,
password: ruleForm.password,
key: verifyData.key,
dots: verifyData.dots,
x: verifyData.x,
})
.then((res) => {
setUserToken(res.data.token)
store.setIsLogin(true)
router.back()
})
.catch((e) => {
showMessageError('登录失败,' + e.message)
})
}
const agreementError = ref(false)
const isShaking = ref(false)
const handleAgreementChange = () => {
agreementError.value = !ruleForm.agreement
if (agreementError.value) {
isShaking.value = true
setTimeout(() => {
isShaking.value = false
}, 500)
}
}
const openAgreement = () => {
// 使用弹窗显示用户协议内容支持Markdown格式
ElMessageBox.alert(
`<div class="markdown-content">${md.render(agreementContent.value)}</div>`,
'用户协议',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {},
}
)
}
const openPrivacy = () => {
// 使用弹窗显示隐私政策内容支持Markdown格式
ElMessageBox.alert(
`<div class="markdown-content">${md.render(privacyContent.value)}</div>`,
'隐私政策',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {},
}
)
}
</script>
<style lang="scss" scoped>
@use '../assets/css/login.scss' as *;
.agreement-box {
margin-bottom: 10px;
transition: all 0.3s;
}
.agreement-link {
color: var(--el-color-primary);
cursor: pointer;
}
.agreement-error {
.el-checkbox {
.el-checkbox__input {
.el-checkbox__inner {
border-color: #f56c6c !important;
}
}
}
}
.shake {
animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
</style>
<style>
/* 全局样式用于Markdown内容显示 */
.markdown-content {
text-align: left;
max-height: 60vh;
overflow-y: auto;
padding: 10px;
}
.markdown-content h1 {
font-size: 1.5em;
margin-bottom: 15px;
}
.markdown-content h2 {
font-size: 1.3em;
margin: 15px 0 10px;
}
.markdown-content p {
margin-bottom: 10px;
line-height: 1.5;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin-bottom: 10px;
}
.markdown-content blockquote {
border-left: 4px solid #ccc;
padding-left: 10px;
color: #666;
margin: 10px 0;
}
.markdown-content code {
background-color: #f0f0f0;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
.markdown-content pre {
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
</style>

View File

@@ -98,7 +98,7 @@ const doLogin = (userId) => {
type: 'error', type: 'error',
title: '登录失败', title: '登录失败',
callback: () => { callback: () => {
router.push('/login') router.push('/')
}, },
}) })
}) })

View File

@@ -1,510 +0,0 @@
<template>
<div>
<div class="flex-center loginPage">
<div class="left" v-if="enableRegister">
<div class="login-box">
<AccountTop title="注册" />
<div class="input-form">
<el-form :model="data" class="form">
<el-tabs v-model="activeName">
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
<el-form-item>
<div class="form-title">手机号码</div>
<el-input
placeholder="请输入手机号码"
size="large"
v-model="data.mobile"
maxlength="11"
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item>
<div class="form-title">验证码</div>
<div class="flex w100">
<el-input
placeholder="请输入验证码"
size="large"
maxlength="30"
class="code-input"
v-model="data.code"
autocomplete="off"
>
</el-input>
<send-msg size="large" :receiver="data.mobile" type="mobile" />
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="邮箱注册" name="email" v-if="enableEmail">
<el-form-item class="block">
<div class="form-title">邮箱</div>
<el-input
placeholder="请输入邮箱地址"
size="large"
v-model="data.email"
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item class="block">
<div class="form-title">验证码</div>
<div class="flex w100">
<el-input
placeholder="请输入验证码"
size="large"
maxlength="30"
class="code-input"
v-model="data.code"
autocomplete="off"
>
</el-input>
<send-msg size="large" :receiver="data.email" type="email" />
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="用户名注册" name="username" v-if="enableUser">
<el-form-item class="block">
<div class="form-title">用户名</div>
<el-input
placeholder="请输入用户名"
size="large"
v-model="data.username"
autocomplete="off"
>
</el-input>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item class="block">
<div class="form-title">密码</div>
<el-input
placeholder="请输入密码(8-16位)"
maxlength="16"
size="large"
v-model="data.password"
show-password
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item class="block">
<div class="form-title">重复密码</div>
<el-input
placeholder="请再次输入密码(8-16位)"
size="large"
maxlength="16"
v-model="data.repass"
show-password
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item class="block">
<div class="form-title">邀请码</div>
<el-input
placeholder="请输入邀请码(可选)"
size="large"
v-model="data.invite_code"
autocomplete="off"
>
</el-input>
</el-form-item>
<el-form-item
label=""
prop="agreement"
:class="{ 'agreement-error': agreementError }"
>
<div class="agreement-box" :class="{ shake: isShaking }">
<el-checkbox v-model="data.agreement" @change="handleAgreementChange">
我已阅读并同意
<span class="agreement-link" @click.stop.prevent="openAgreement"
>用户协议</span
>
<span class="agreement-link" @click.stop.prevent="openPrivacy"
>隐私政策</span
>
</el-checkbox>
</div>
</el-form-item>
<el-row class="btn-row" :gutter="20">
<el-col :span="24">
<el-button class="login-btn" type="primary" size="large" @click="submitRegister"
>注册</el-button
>
</el-col>
</el-row>
</el-form>
</div>
</div>
</div>
<div class="tip-result left" v-else>
<el-result icon="error" title="注册功能已关闭">
<template #sub-title>
<p>抱歉系统已关闭注册功能请联系管理员添加账号</p>
<div class="wechat-card">
<el-image :src="wxImg" />
</div>
<div class="mt-3">
<el-button type="primary" @click="router.push('/')"
><i class="iconfont icon-home mr-1"></i> 返回首页</el-button
>
</div>
</template>
</el-result>
</div>
<captcha v-if="enableVerify" @success="doSubmitRegister" ref="captchaRef" />
<account-bg />
</div>
</div>
</template>
<script setup>
import AccountBg from '@/components/AccountBg.vue'
import AccountTop from '@/components/AccountTop.vue'
import { ref } from 'vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage, ElMessageBox } from 'element-plus'
import MarkdownIt from 'markdown-it'
import { useRouter } from 'vue-router'
import Captcha from '@/components/Captcha.vue'
import SendMsg from '@/components/SendMsg.vue'
import { getLicenseInfo, getSystemInfo } from '@/store/cache'
import { setUserToken } from '@/store/session'
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { arrayContains, isMobile } from '@/utils/libs'
import { validateEmail, validateMobile } from '@/utils/validate'
const router = useRouter()
const title = ref('')
const logo = ref('')
const data = ref({
username: '',
mobile: '',
email: '',
password: '',
code: '',
repass: '',
invite_code: router.currentRoute.value.query['invite_code'],
agreement: false,
})
const enableMobile = ref(false)
const enableEmail = ref(false)
const enableUser = ref(false)
const enableRegister = ref(true)
const activeName = ref('mobile')
const wxImg = ref('/images/wx.png')
const licenseConfig = ref({})
const enableVerify = ref(false)
const captchaRef = ref(null)
const agreementError = ref(false)
const isShaking = ref(false)
// 初始化markdown解析器
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
})
const agreementContent = ref('')
const privacyContent = ref('')
// 记录邀请码点击次数
if (data.value.invite_code) {
httpGet('/api/invite/hits', { code: data.value.invite_code })
}
getSystemInfo()
.then((res) => {
if (res.data) {
title.value = res.data.title
logo.value = res.data.logo
const registerWays = res.data['register_ways']
if (arrayContains(registerWays, 'username')) {
enableUser.value = true
activeName.value = 'username'
}
if (arrayContains(registerWays, 'email')) {
enableEmail.value = true
activeName.value = 'email'
}
if (arrayContains(registerWays, 'mobile')) {
enableMobile.value = true
activeName.value = 'mobile'
}
// 是否启用注册
enableRegister.value = res.data['enabled_register']
// 使用后台上传的客服微信二维码
if (res.data['wechat_card_url'] !== '') {
wxImg.value = res.data['wechat_card_url']
}
enableVerify.value = res.data['enabled_verify']
}
})
.catch((e) => {
ElMessage.error('获取系统配置失败:' + e.message)
})
// 获取用户协议
httpGet('/api/config/get?key=agreement')
.then((res) => {
if (res.data && res.data.content) {
agreementContent.value = res.data.content
} else {
agreementContent.value =
'# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分与协议正文具有同等法律效力。'
}
})
.catch((e) => {
console.warn(e)
agreementContent.value =
'# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分与协议正文具有同等法律效力。'
})
// 获取隐私政策
httpGet('/api/config/get?key=privacy')
.then((res) => {
if (res.data && res.data.content) {
privacyContent.value = res.data.content
} else {
privacyContent.value =
'# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
}
})
.catch((e) => {
console.warn(e)
privacyContent.value =
'# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
})
getLicenseInfo()
.then((res) => {
licenseConfig.value = res.data
})
.catch((e) => {
showMessageError('获取 License 配置:' + e.message)
})
// 注册操作
const submitRegister = () => {
if (activeName.value === 'username' && data.value.username === '') {
return showMessageError('请输入用户名')
}
if (activeName.value === 'mobile' && !validateMobile(data.value.mobile)) {
return showMessageError('请输入合法的手机号')
}
if (activeName.value === 'email' && !validateEmail(data.value.email)) {
return showMessageError('请输入合法的邮箱地址')
}
if (data.value.password.length < 8) {
return showMessageError('密码的长度为8-16个字符')
}
if (data.value.repass !== data.value.password) {
return showMessageError('两次输入密码不一致')
}
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
return showMessageError('请输入验证码')
}
if (!data.value.agreement) {
agreementError.value = true
isShaking.value = true
setTimeout(() => {
isShaking.value = false
}, 500)
showMessageError('请先阅读并同意用户协议和隐私政策')
return
}
// 如果是用户名和密码登录,那么需要加载验证码
if (enableVerify.value && activeName.value === 'username') {
captchaRef.value.loadCaptcha()
} else {
doSubmitRegister({})
}
}
const doSubmitRegister = (verifyData) => {
data.value.key = verifyData.key
data.value.dots = verifyData.dots
data.value.x = verifyData.x
data.value.reg_way = activeName.value
httpPost('/api/user/register', data.value)
.then((res) => {
setUserToken(res.data.token)
showMessageOK('注册成功,即将跳转到对话主界面...')
if (isMobile()) {
router.push('/mobile/index')
} else {
router.push('/chat')
}
})
.catch((e) => {
showMessageError('注册失败,' + e.message)
})
}
const handleAgreementChange = () => {
agreementError.value = !data.value.agreement
}
const openAgreement = () => {
// 使用弹窗显示用户协议内容支持Markdown格式
ElMessageBox.alert(
`<div class="markdown-content">${md.render(agreementContent.value)}</div>`,
'用户协议',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {},
}
)
}
const openPrivacy = () => {
// 使用弹窗显示隐私政策内容支持Markdown格式
ElMessageBox.alert(
`<div class="markdown-content">${md.render(privacyContent.value)}</div>`,
'隐私政策',
{
confirmButtonText: '我已阅读',
dangerouslyUseHTMLString: true,
callback: () => {},
}
)
}
</script>
<style lang="scss" scoped>
@use '../assets/css/login.scss' as *;
:deep(.back) {
margin-bottom: 10px;
}
:deep(.orline) {
margin-bottom: 10px;
}
.wechat-card {
margin-top: 20px;
}
.agreement-box {
margin-bottom: 10px;
transition: all 0.3s;
}
.agreement-link {
color: var(--el-color-primary);
cursor: pointer;
}
.agreement-error {
.el-checkbox {
.el-checkbox__input {
.el-checkbox__inner {
border-color: #f56c6c !important;
}
}
}
}
.shake {
animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
</style>
<style>
/* 全局样式用于Markdown内容显示 */
.markdown-content {
text-align: left;
max-height: 60vh;
overflow-y: auto;
padding: 10px;
}
.markdown-content h1 {
font-size: 1.5em;
margin-bottom: 15px;
}
.markdown-content h2 {
font-size: 1.3em;
margin: 15px 0 10px;
}
.markdown-content p {
margin-bottom: 10px;
line-height: 1.5;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin-bottom: 10px;
}
.markdown-content blockquote {
border-left: 4px solid #ccc;
padding-left: 10px;
color: #666;
margin: 10px 0;
}
.markdown-content code {
background-color: #f0f0f0;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
.markdown-content pre {
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
margin: 10px 0;
}
</style>

View File

@@ -1,10 +1,8 @@
<template> <template>
<div class="apps-page"> <div class="apps-page">
<van-nav-bar title="全部应用" left-arrow @click-left="router.back()" /> <div class="apps-filter mb-8 px-3">
<CustomTabs :model-value="activeTab" @update:model-value="activeTab = $event">
<div class="apps-filter mb-8 pt-8" style="border: 1px solid #ccc"> <CustomTabPane name="all" label="全部分类">
<van-tabs v-model="activeTab" animated swipeable>
<van-tab title="全部分类">
<div class="app-list"> <div class="app-list">
<van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps()"> <van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps()">
<van-cell v-for="item in apps" :key="item.id" class="app-cell"> <van-cell v-for="item in apps" :key="item.id" class="app-cell">
@@ -40,8 +38,8 @@
</van-cell> </van-cell>
</van-list> </van-list>
</div> </div>
</van-tab> </CustomTabPane>
<van-tab v-for="type in appTypes" :key="type.id" :title="type.name"> <CustomTabPane v-for="type in appTypes" :key="type.id" :name="type.id" :label="type.name">
<div class="app-list"> <div class="app-list">
<van-list <van-list
v-model="loading" v-model="loading"
@@ -82,13 +80,15 @@
</van-cell> </van-cell>
</van-list> </van-list>
</div> </div>
</van-tab> </CustomTabPane>
</van-tabs> </CustomTabs>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
import CustomTabs from '@/components/ui/CustomTabs.vue'
import { checkSession } from '@/store/cache' import { checkSession } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http' import { httpGet, httpPost } from '@/utils/http'
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs' import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
@@ -151,16 +151,16 @@ const updateRole = (row, opt) => {
return showLoginDialog(router) return showLoginDialog(router)
} }
const title = ref('') let actionTitle = ''
if (opt === 'add') { if (opt === 'add') {
title.value = '添加应用' actionTitle = '添加应用'
const exists = arrayContains(roles.value, row.key) const exists = arrayContains(roles.value, row.key)
if (exists) { if (exists) {
return return
} }
roles.value.push(row.key) roles.value.push(row.key)
} else { } else {
title.value = '移除应用' actionTitle = '移除应用'
const exists = arrayContains(roles.value, row.key) const exists = arrayContains(roles.value, row.key)
if (!exists) { if (!exists) {
return return
@@ -169,10 +169,10 @@ const updateRole = (row, opt) => {
} }
httpPost('/api/app/update', { keys: roles.value }) httpPost('/api/app/update', { keys: roles.value })
.then(() => { .then(() => {
showNotify({ type: 'success', message: title.value + '成功!' }) showNotify({ type: 'success', message: actionTitle + '成功!' })
}) })
.catch((e) => { .catch((e) => {
showNotify({ type: 'danger', message: title.value + '失败:' + e.message }) showNotify({ type: 'danger', message: actionTitle + '失败:' + e.message })
}) })
} }
@@ -194,15 +194,13 @@ const useRole = (roleId) => {
background-color: var(--van-background); background-color: var(--van-background);
.apps-filter { .apps-filter {
padding: 10px 0;
:deep(.van-tabs__nav) { :deep(.van-tabs__nav) {
background: var(--van-background-2); background: var(--van-background-2);
} }
} }
.app-list { .app-list {
padding: 0 15px; padding: 0;
.app-cell { .app-cell {
padding: 0; padding: 0;

View File

@@ -75,14 +75,15 @@
</template> </template>
<script setup> <script setup>
import { router } from '@/router'
import { checkSession } from '@/store/cache' import { checkSession } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http' import { httpGet, httpPost } from '@/utils/http'
import { removeArrayItem, showLoginDialog } from '@/utils/libs' import { removeArrayItem, showLoginDialog } from '@/utils/libs'
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant' import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router'
const title = ref('会话列表') const title = ref('会话列表')
const router = useRouter()
const chatName = ref('') const chatName = ref('')
const chats = ref([]) const chats = ref([])
const allChats = ref([]) const allChats = ref([])

View File

@@ -161,7 +161,7 @@ checkSession()
loginUser.value = user loginUser.value = user
}) })
.catch(() => { .catch(() => {
router.push('/login') router.push('/mobile/login')
}) })
const loadModels = () => { const loadModels = () => {

View File

@@ -1,68 +1,287 @@
<template> <template>
<div class="create-center"> <div class="create-center">
<van-nav-bar title="AI 创作中心" fixed :safe-area-inset-top="true"> <div class="create-content px-3">
<template #left> <CustomTabs
<div class="nav-left"> :model-value="activeTab"
<i class="iconfont icon-mj"></i> @update:model-value="activeTab = $event"
</div> @tab-click="onTabChange"
</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"> <CustomTabPane name="mj" label="MJ绘画">
<div class="tab-content"> <div class="tab-content">
<image-mj /> <image-mj />
</div> </div>
</van-tab> </CustomTabPane>
<van-tab title="SD绘画" name="sd" v-if="activeMenu.sd"> <CustomTabPane name="sd" label="SD绘画">
<div class="tab-content"> <div class="tab-content">
<image-sd /> <image-sd />
</div> </div>
</van-tab> </CustomTabPane>
<van-tab title="DALL·E" name="dalle" v-if="activeMenu.dall"> <CustomTabPane name="dalle" label="DALL·E">
<div class="tab-content"> <div class="tab-content">
<image-dall /> <image-dall />
</div> </div>
</van-tab> </CustomTabPane>
<van-tab title="音乐创作" name="suno" v-if="activeMenu.suno"> <CustomTabPane name="suno" label="音乐创作">
<div class="tab-content"> <div class="tab-content">
<suno-create /> <suno-create />
</div> </div>
</van-tab> </CustomTabPane>
<van-tab title="视频生成" name="video" v-if="activeMenu.video"> <CustomTabPane name="video" label="视频生成">
<div class="tab-content"> <div class="tab-content">
<video-create /> <video-create />
</div> </div>
</van-tab> </CustomTabPane>
<van-tab title="即梦AI" name="jimeng" v-if="activeMenu.jimeng"> <CustomTabPane name="jimeng" label="即梦AI">
<div class="tab-content"> <div class="tab-content">
<jimeng-create /> <jimeng-create />
</div> </div>
</van-tab> </CustomTabPane>
</van-tabs> </CustomTabs>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
import CustomTabs from '@/components/ui/CustomTabs.vue'
import { httpGet } from '@/utils/http' import { httpGet } from '@/utils/http'
import ImageDall from '@/views/mobile/pages/ImageDall.vue' import ImageDall from '@/views/mobile/pages/ImageDall.vue'
import ImageMj from '@/views/mobile/pages/ImageMj.vue' import ImageMj from '@/views/mobile/pages/ImageMj.vue'
import ImageSd from '@/views/mobile/pages/ImageSd.vue' import ImageSd from '@/views/mobile/pages/ImageSd.vue'
import { onMounted, ref, watch } from 'vue' import { Button, Field, Image, showNotify } from 'vant'
import { h, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
// 临时组件,实际项目中需要创建对应的移动端组件 // 创建缺失的移动端组件
const SunoCreate = { template: '<div class="placeholder">Suno音乐创作功能开发中...</div>' } const SunoCreate = {
const VideoCreate = { template: '<div class="placeholder">视频生成功能开发中...</div>' } name: 'SunoCreate',
const JimengCreate = { template: '<div class="placeholder">即梦AI功能开发中...</div>' } setup() {
const prompt = ref('')
const duration = ref(30)
const loading = ref(false)
const result = ref('')
const generateMusic = () => {
if (!prompt.value.trim()) {
showNotify({ type: 'warning', message: '请输入音乐描述' })
return
}
loading.value = true
// TODO: 调用Suno API
setTimeout(() => {
loading.value = false
showNotify({ type: 'success', message: '音乐生成功能开发中' })
}, 2000)
}
const downloadMusic = () => {
// TODO: 实现下载功能
showNotify({ type: 'primary', message: '下载功能开发中' })
}
return () =>
h('div', { class: 'suno-create' }, [
h('div', { class: 'create-header' }, [h('h3', '音乐创作'), h('p', 'AI驱动的音乐生成工具')]),
h('div', { class: 'create-form' }, [
h(Field, {
value: prompt.value,
onInput: (val) => {
prompt.value = val
},
type: 'textarea',
placeholder: '描述您想要的音乐风格、情感或主题...',
rows: 4,
maxlength: 500,
'show-word-limit': true,
}),
h(Field, {
value: duration.value,
onInput: (val) => {
duration.value = val
},
label: '时长',
type: 'number',
placeholder: '音乐时长(秒)',
}),
h(
Button,
{
type: 'primary',
size: 'large',
block: true,
loading: loading.value,
onClick: generateMusic,
},
'生成音乐'
),
]),
result.value
? h('div', { class: 'result-area' }, [
h('h4', '生成结果'),
h('audio', { src: result.value, controls: true }),
h(Button, { size: 'small', onClick: downloadMusic }, '下载'),
])
: null,
])
},
}
const VideoCreate = {
name: 'VideoCreate',
setup() {
const prompt = ref('')
const duration = ref(10)
const loading = ref(false)
const result = ref('')
const generateVideo = () => {
if (!prompt.value.trim()) {
showNotify({ type: 'warning', message: '请输入视频描述' })
return
}
loading.value = true
// TODO: 调用视频生成API
setTimeout(() => {
loading.value = false
showNotify({ type: 'success', message: '视频生成功能开发中' })
}, 2000)
}
const downloadVideo = () => {
// TODO: 实现下载功能
showNotify({ type: 'primary', message: '下载功能开发中' })
}
return () =>
h('div', { class: 'video-create' }, [
h('div', { class: 'create-header' }, [h('h3', '视频生成'), h('p', 'AI驱动的视频创作工具')]),
h('div', { class: 'create-form' }, [
h(Field, {
value: prompt.value,
onInput: (val) => {
prompt.value = val
},
type: 'textarea',
placeholder: '描述您想要的视频内容、风格或场景...',
rows: 4,
maxlength: 500,
'show-word-limit': true,
}),
h(Field, {
value: duration.value,
onInput: (val) => {
duration.value = val
},
label: '时长',
type: 'number',
placeholder: '视频时长(秒)',
}),
h(
Button,
{
type: 'primary',
size: 'large',
block: true,
loading: loading.value,
onClick: generateVideo,
},
'生成视频'
),
]),
result.value
? h('div', { class: 'result-area' }, [
h('h4', '生成结果'),
h('video', { src: result.value, controls: true }),
h(Button, { size: 'small', onClick: downloadVideo }, '下载'),
])
: null,
])
},
}
const JimengCreate = {
name: 'JimengCreate',
setup() {
const prompt = ref('')
const negativePrompt = ref('')
const steps = ref(20)
const loading = ref(false)
const result = ref('')
const generateImage = () => {
if (!prompt.value.trim()) {
showNotify({ type: 'warning', message: '请输入图像描述' })
return
}
loading.value = true
// TODO: 调用即梦AI API
setTimeout(() => {
loading.value = false
showNotify({ type: 'success', message: '即梦AI功能开发中' })
}, 2000)
}
const downloadImage = () => {
// TODO: 实现下载功能
showNotify({ type: 'primary', message: '下载功能开发中' })
}
return () =>
h('div', { class: 'jimeng-create' }, [
h('div', { class: 'create-header' }, [h('h3', '即梦AI'), h('p', '专业的AI图像生成工具')]),
h('div', { class: 'create-form' }, [
h(Field, {
value: prompt.value,
onInput: (val) => {
prompt.value = val
},
type: 'textarea',
placeholder: '描述您想要的图像内容...',
rows: 4,
maxlength: 500,
'show-word-limit': true,
}),
h(Field, {
value: negativePrompt.value,
onInput: (val) => {
negativePrompt.value = val
},
type: 'textarea',
placeholder: '负面提示词(可选)',
rows: 2,
maxlength: 200,
}),
h(Field, {
value: steps.value,
onInput: (val) => {
steps.value = val
},
label: '步数',
type: 'number',
placeholder: '生成步数',
}),
h(
Button,
{
type: 'primary',
size: 'large',
block: true,
loading: loading.value,
onClick: generateImage,
},
'生成图像'
),
]),
result.value
? h('div', { class: 'result-area' }, [
h('h4', '生成结果'),
h(Image, { src: result.value, fit: 'cover' }),
h(Button, { size: 'small', onClick: downloadImage }, '下载'),
])
: null,
])
},
}
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -78,17 +297,21 @@ const activeMenu = ref({
}) })
// 监听路由参数变化 // 监听路由参数变化
watch(() => route.query.tab, (newTab) => { watch(
if (newTab && activeMenu.value[newTab]) { () => route.query.tab,
activeTab.value = newTab (newTab) => {
} if (newTab && activeMenu.value[newTab]) {
}, { immediate: true }) activeTab.value = newTab
}
},
{ immediate: true }
)
// Tab切换处理 // Tab切换处理
const onTabChange = (name) => { const onTabChange = (name) => {
router.replace({ router.replace({
path: route.path, path: route.path,
query: { ...route.query, tab: name } query: { ...route.query, tab: name },
}) })
} }
@@ -97,27 +320,31 @@ onMounted(() => {
}) })
const fetchMenus = () => { const fetchMenus = () => {
httpGet('/api/menu/list').then((res) => { httpGet('/api/menu/list')
menus.value = res.data .then((res) => {
activeMenu.value = { console.log(res)
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默认选择第一个可用的 menus.value = res.data
if (!route.query.tab) { activeMenu.value = {
const firstAvailable = Object.keys(activeMenu.value).find(key => activeMenu.value[key]) mj: menus.value.some((item) => item.url === '/mj'),
if (firstAvailable) { sd: menus.value.some((item) => item.url === '/sd'),
activeTab.value = firstAvailable 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'),
} }
}
}).catch((e) => { // 如果没有指定tab默认选择第一个可用的
console.error('获取菜单失败:', e.message) 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> </script>
@@ -137,8 +364,6 @@ const fetchMenus = () => {
} }
.create-content { .create-content {
padding-top: 44px; // nav-bar height
:deep(.van-tabs__nav) { :deep(.van-tabs__nav) {
background: var(--van-background); background: var(--van-background);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
@@ -163,6 +388,70 @@ const fetchMenus = () => {
color: var(--van-gray-6); color: var(--van-gray-6);
font-size: 16px; font-size: 16px;
} }
// 新增组件样式
.suno-create,
.video-create,
.jimeng-create {
padding: 20px;
.create-header {
text-align: center;
margin-bottom: 24px;
h3 {
font-size: 20px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: var(--van-gray-6);
margin: 0;
}
}
.create-form {
margin-bottom: 24px;
.van-field {
margin-bottom: 16px;
}
.van-button {
margin-top: 16px;
}
}
.result-area {
background: var(--van-cell-background);
border-radius: 12px;
padding: 16px;
text-align: center;
h4 {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 12px 0;
}
audio,
video,
.van-image {
width: 100%;
max-width: 300px;
margin-bottom: 12px;
border-radius: 8px;
}
.van-button {
margin-top: 8px;
}
}
}
} }
} }
} }

View File

@@ -1,13 +1,5 @@
<template> <template>
<div class="discover-page"> <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="discover-content">
<!-- 功能分类 --> <!-- 功能分类 -->
<div class="category-section"> <div class="category-section">
@@ -90,8 +82,8 @@
</template> </template>
<script setup> <script setup>
import { useRouter } from 'vue-router'
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
@@ -99,28 +91,114 @@ const router = useRouter()
const aiTools = ref([ const aiTools = ref([
{ key: 'mj', name: 'MJ绘画', icon: 'icon-mj', color: '#8B5CF6', url: '/mobile/create?tab=mj' }, { 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: '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: 'dalle',
{ key: 'video', name: '视频生成', icon: 'icon-video', color: '#10B981', url: '/mobile/create?tab=video' }, name: 'DALL·E',
{ key: 'jimeng', name: '即梦AI', icon: 'icon-jimeng', color: '#F97316', url: '/mobile/create?tab=jimeng' }, icon: 'icon-dalle',
{ key: 'xmind', name: '思维导图', icon: 'icon-mind', color: '#3B82F6', url: '/mobile/tools?tab=xmind' }, color: '#F59E0B',
{ key: 'apps', name: '应用中心', icon: 'icon-apps', color: '#EC4899', url: '/mobile/apps' } 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([ 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: 'member',
{ key: 'invite', name: '邀请好友', desc: '推广获取奖励', icon: 'icon-user-plus', color: '#F59E0B', url: '/mobile/invite' }, name: '会员中心',
{ key: 'export', name: '导出对话', desc: '保存聊天记录', icon: 'icon-download', color: '#06B6D4', url: '/mobile/chat/export' } 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([ 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: 'imgWall',
{ key: 'help', name: '帮助中心', desc: '使用指南和常见问题', icon: 'icon-help', color: '#8B5CF6', url: '/mobile/help' }, name: '作品展示',
{ key: 'feedback', name: '意见反馈', desc: '提出建议和问题', icon: 'icon-message', color: '#EF4444', url: '/mobile/feedback' } 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',
},
]) ])
// 推荐内容 // 推荐内容
@@ -130,29 +208,29 @@ const recommendations = ref([
title: '新功能发布', title: '新功能发布',
desc: '体验最新AI创作工具', desc: '体验最新AI创作工具',
image: '/images/recommend/new-features.jpg', image: '/images/recommend/new-features.jpg',
url: '/mobile/news' url: '/mobile/news',
}, },
{ {
key: 'tutorials', key: 'tutorials',
title: '使用教程', title: '使用教程',
desc: '快速上手AI创作', desc: '快速上手AI创作',
image: '/images/recommend/tutorials.jpg', image: '/images/recommend/tutorials.jpg',
url: '/mobile/tutorials' url: '/mobile/tutorials',
}, },
{ {
key: 'gallery', key: 'gallery',
title: '精选作品', title: '精选作品',
desc: '欣赏优秀AI作品', desc: '欣赏优秀AI作品',
image: '/images/recommend/gallery.jpg', image: '/images/recommend/gallery.jpg',
url: '/mobile/imgWall' url: '/mobile/imgWall',
}, },
{ {
key: 'community', key: 'community',
title: '用户社区', title: '用户社区',
desc: '交流创作心得', desc: '交流创作心得',
image: '/images/recommend/community.jpg', image: '/images/recommend/community.jpg',
url: '/mobile/community' url: '/mobile/community',
} },
]) ])
// 导航处理 // 导航处理
@@ -181,8 +259,6 @@ const navigateTo = (url) => {
} }
.discover-content { .discover-content {
padding: 54px 16px 60px; // nav-bar height + bottom padding
.category-section { .category-section {
margin-bottom: 24px; margin-bottom: 24px;

View File

@@ -1,8 +1,5 @@
<template> <template>
<div class="mobile-feedback"> <div class="mobile-feedback">
<!-- 顶部导航 -->
<van-nav-bar title="意见反馈" left-arrow @click-left="router.back()" fixed placeholder />
<!-- 反馈表单 --> <!-- 反馈表单 -->
<div class="feedback-content"> <div class="feedback-content">
<!-- 反馈类型 --> <!-- 反馈类型 -->

View File

@@ -1,11 +1,5 @@
<template> <template>
<div class="help-page"> <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="help-content">
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="search-section" v-if="showSearch"> <div class="search-section" v-if="showSearch">
@@ -107,7 +101,12 @@
<span class="online-status">在线</span> <span class="online-status">在线</span>
</template> </template>
</van-cell> </van-cell>
<van-cell title="意见反馈" icon="chat-o" is-link @click="router.push('/mobile/feedback')" /> <van-cell
title="意见反馈"
icon="chat-o"
is-link
@click="router.push('/mobile/feedback')"
/>
<van-cell title="官方QQ群" icon="friends-o" is-link @click="joinQQGroup"> <van-cell title="官方QQ群" icon="friends-o" is-link @click="joinQQGroup">
<template #value> <template #value>
<span class="qq-number">123456789</span> <span class="qq-number">123456789</span>
@@ -160,7 +159,11 @@
</van-dialog> </van-dialog>
<!-- 客服聊天 --> <!-- 客服聊天 -->
<van-action-sheet v-model:show="showCustomerChat" title="在线客服" :close-on-click-overlay="false"> <van-action-sheet
v-model:show="showCustomerChat"
title="在线客服"
:close-on-click-overlay="false"
>
<div class="customer-chat"> <div class="customer-chat">
<div class="chat-header"> <div class="chat-header">
<div class="customer-info"> <div class="customer-info">
@@ -226,28 +229,33 @@ const frequentFAQs = ref([
{ {
id: 1, id: 1,
question: '如何获得算力?', question: '如何获得算力?',
answer: '<p>您可以通过以下方式获得算力:</p><ul><li>注册即送算力</li><li>购买充值套餐</li><li>邀请好友注册</li><li>参与活动获得</li></ul>' answer:
'<p>您可以通过以下方式获得算力:</p><ul><li>注册即送算力</li><li>购买充值套餐</li><li>邀请好友注册</li><li>参与活动获得</li></ul>',
}, },
{ {
id: 2, id: 2,
question: '如何使用AI绘画功能', question: '如何使用AI绘画功能',
answer: '<p>使用AI绘画功能很简单</p><ol><li>进入创作中心</li><li>选择绘画工具MJ、SD、DALL-E</li><li>输入描述文字</li><li>点击生成即可</li></ol>' answer:
'<p>使用AI绘画功能很简单</p><ol><li>进入创作中心</li><li>选择绘画工具MJ、SD、DALL-E</li><li>输入描述文字</li><li>点击生成即可</li></ol>',
}, },
{ {
id: 3, id: 3,
question: '为什么生成失败?', question: '为什么生成失败?',
answer: '<p>生成失败可能的原因:</p><ul><li>算力不足</li><li>内容违规</li><li>网络不稳定</li><li>服务器繁忙</li></ul><p>请检查算力余额并重试。</p>' answer:
'<p>生成失败可能的原因:</p><ul><li>算力不足</li><li>内容违规</li><li>网络不稳定</li><li>服务器繁忙</li></ul><p>请检查算力余额并重试。</p>',
}, },
{ {
id: 4, id: 4,
question: '如何成为VIP会员', question: '如何成为VIP会员',
answer: '<p>成为VIP会员的方式</p><ol><li>进入会员中心</li><li>选择合适的套餐</li><li>完成支付</li><li>自动开通VIP权限</li></ol>' answer:
'<p>成为VIP会员的方式</p><ol><li>进入会员中心</li><li>选择合适的套餐</li><li>完成支付</li><li>自动开通VIP权限</li></ol>',
}, },
{ {
id: 5, id: 5,
question: '如何导出聊天记录?', question: '如何导出聊天记录?',
answer: '<p>导出聊天记录步骤:</p><ol><li>进入对话页面</li><li>点击右上角菜单</li><li>选择"导出记录"</li><li>选择导出格式</li><li>确认导出</li></ol>' answer:
} '<p>导出聊天记录步骤:</p><ol><li>进入对话页面</li><li>点击右上角菜单</li><li>选择"导出记录"</li><li>选择导出格式</li><li>确认导出</li></ol>',
},
]) ])
// 功能指南 // 功能指南
@@ -258,7 +266,7 @@ const guides = ref([
desc: '与AI智能对话', desc: '与AI智能对话',
icon: 'icon-chat', icon: 'icon-chat',
color: '#1989fa', color: '#1989fa',
content: 'AI对话使用指南详细内容...' content: 'AI对话使用指南详细内容...',
}, },
{ {
id: 2, id: 2,
@@ -266,7 +274,7 @@ const guides = ref([
desc: '生成精美图片', desc: '生成精美图片',
icon: 'icon-mj', icon: 'icon-mj',
color: '#8B5CF6', color: '#8B5CF6',
content: 'AI绘画使用指南详细内容...' content: 'AI绘画使用指南详细内容...',
}, },
{ {
id: 3, id: 3,
@@ -274,7 +282,7 @@ const guides = ref([
desc: '创作美妙音乐', desc: '创作美妙音乐',
icon: 'icon-music', icon: 'icon-music',
color: '#ee0a24', color: '#ee0a24',
content: 'AI音乐创作指南详细内容...' content: 'AI音乐创作指南详细内容...',
}, },
{ {
id: 4, id: 4,
@@ -282,8 +290,8 @@ const guides = ref([
desc: '制作精彩视频', desc: '制作精彩视频',
icon: 'icon-video', icon: 'icon-video',
color: '#07c160', color: '#07c160',
content: 'AI视频制作指南详细内容...' content: 'AI视频制作指南详细内容...',
} },
]) ])
// 问题分类 // 问题分类
@@ -292,7 +300,7 @@ const categories = ref([
{ id: 2, name: '功能使用', icon: 'icon-apps', count: 23 }, { id: 2, name: '功能使用', icon: 'icon-apps', count: 23 },
{ id: 3, name: '充值支付', icon: 'icon-money', count: 12 }, { id: 3, name: '充值支付', icon: 'icon-money', count: 12 },
{ id: 4, name: '技术问题', icon: 'icon-setting', count: 18 }, { id: 4, name: '技术问题', icon: 'icon-setting', count: 18 },
{ id: 5, name: '其他问题', icon: 'icon-help', count: 8 } { id: 5, name: '其他问题', icon: 'icon-help', count: 8 },
]) ])
// 使用提示 // 使用提示
@@ -301,20 +309,20 @@ const tips = ref([
id: 1, id: 1,
title: '提高绘画质量', title: '提高绘画质量',
content: '使用详细的描述词可以获得更好的绘画效果,建议加入风格、色彩、构图等关键词。', content: '使用详细的描述词可以获得更好的绘画效果,建议加入风格、色彩、构图等关键词。',
icon: 'icon-bulb' icon: 'icon-bulb',
}, },
{ {
id: 2, id: 2,
title: '节省算力', title: '节省算力',
content: '合理使用不同模型简单问题使用GPT-3.5复杂任务使用GPT-4。', content: '合理使用不同模型简单问题使用GPT-3.5复杂任务使用GPT-4。',
icon: 'icon-flash' icon: 'icon-flash',
}, },
{ {
id: 3, id: 3,
title: '快速上手', title: '快速上手',
content: '查看应用中心的预设角色可以快速体验不同类型的AI对话。', content: '查看应用中心的预设角色可以快速体验不同类型的AI对话。',
icon: 'icon-star' icon: 'icon-star',
} },
]) ])
onMounted(() => { onMounted(() => {
@@ -324,8 +332,8 @@ onMounted(() => {
id: 1, id: 1,
content: '您好欢迎使用我们的AI创作平台有什么可以帮助您的吗', content: '您好欢迎使用我们的AI创作平台有什么可以帮助您的吗',
isUser: false, isUser: false,
time: new Date() time: new Date(),
} },
] ]
}) })
@@ -338,27 +346,25 @@ const onSearch = (keyword) => {
// 模拟搜索结果 // 模拟搜索结果
const allContent = [ const allContent = [
...frequentFAQs.value.map(faq => ({ ...frequentFAQs.value.map((faq) => ({
id: faq.id, id: faq.id,
title: faq.question, title: faq.question,
content: faq.answer, content: faq.answer,
type: 'faq' type: 'faq',
})), })),
...guides.value.map(guide => ({ ...guides.value.map((guide) => ({
id: guide.id, id: guide.id,
title: guide.title, title: guide.title,
content: guide.content, content: guide.content,
type: 'guide' type: 'guide',
})) })),
] ]
searchResults.value = allContent searchResults.value = allContent
.filter(item => .filter((item) => item.title.includes(keyword) || item.content.includes(keyword))
item.title.includes(keyword) || item.content.includes(keyword) .map((item) => ({
)
.map(item => ({
...item, ...item,
snippet: getSearchSnippet(item.content, keyword) snippet: getSearchSnippet(item.content, keyword),
})) }))
} }
@@ -383,7 +389,7 @@ const getSearchSnippet = (content, keyword) => {
const openGuide = (guide) => { const openGuide = (guide) => {
selectedHelp.value = { selectedHelp.value = {
title: guide.title, title: guide.title,
content: guide.content || '<p>该指南内容正在完善中,敬请期待。</p>' content: guide.content || '<p>该指南内容正在完善中,敬请期待。</p>',
} }
showHelpDetail.value = true showHelpDetail.value = true
} }
@@ -398,7 +404,7 @@ const openCategory = (category) => {
const openSearchResult = (result) => { const openSearchResult = (result) => {
selectedHelp.value = { selectedHelp.value = {
title: result.title, title: result.title,
content: result.content content: result.content,
} }
showHelpDetail.value = true showHelpDetail.value = true
} }
@@ -414,7 +420,7 @@ const shareHelp = (help) => {
navigator.share({ navigator.share({
title: help.title, title: help.title,
text: help.content.replace(/<[^>]*>/g, ''), text: help.content.replace(/<[^>]*>/g, ''),
url: window.location.href url: window.location.href,
}) })
} else { } else {
showNotify({ type: 'primary', message: '该功能暂不支持' }) showNotify({ type: 'primary', message: '该功能暂不支持' })
@@ -435,7 +441,7 @@ const sendCustomerMessage = () => {
id: Date.now(), id: Date.now(),
content: customerMessage.value, content: customerMessage.value,
isUser: true, isUser: true,
time: new Date() time: new Date(),
}) })
const userMessage = customerMessage.value const userMessage = customerMessage.value
@@ -464,7 +470,7 @@ const sendCustomerMessage = () => {
id: Date.now(), id: Date.now(),
content: reply, content: reply,
isUser: false, isUser: false,
time: new Date() time: new Date(),
}) })
nextTick(() => { nextTick(() => {
@@ -478,7 +484,8 @@ const sendCustomerMessage = () => {
// 加入QQ群 // 加入QQ群
const joinQQGroup = () => { const joinQQGroup = () => {
// 尝试打开QQ群链接 // 尝试打开QQ群链接
const qqGroupUrl = 'mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D123456789' 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 window.location.href = qqGroupUrl
setTimeout(() => { setTimeout(() => {
@@ -490,7 +497,7 @@ const joinQQGroup = () => {
const formatTime = (time) => { const formatTime = (time) => {
return time.toLocaleTimeString('zh-CN', { return time.toLocaleTimeString('zh-CN', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit',
}) })
} }
@@ -541,7 +548,8 @@ const onQRError = (e) => {
color: var(--van-gray-7); color: var(--van-gray-7);
line-height: 1.6; line-height: 1.6;
:deep(ul), :deep(ol) { :deep(ul),
:deep(ol) {
padding-left: 20px; padding-left: 20px;
margin: 8px 0; margin: 8px 0;
} }
@@ -664,7 +672,7 @@ const onQRError = (e) => {
overflow: hidden; overflow: hidden;
.tip-card { .tip-card {
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6); background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
color: white; color: white;
padding: 20px; padding: 20px;
text-align: center; text-align: center;
@@ -713,7 +721,8 @@ const onQRError = (e) => {
margin: 8px 0; margin: 8px 0;
} }
:deep(ul), :deep(ol) { :deep(ul),
:deep(ol) {
padding-left: 20px; padding-left: 20px;
margin: 8px 0; margin: 8px 0;
} }

View File

@@ -1,7 +1,13 @@
<template> <template>
<van-config-provider :theme="theme"> <van-config-provider :theme="theme">
<div class="mobile-home"> <div class="mobile-home">
<router-view /> <div class="page-content">
<router-view :key="routerViewKey" v-slot="{ Component }">
<transition name="move" mode="out-in">
<component :is="Component"></component>
</transition>
</router-view>
</div>
<van-tabbar route v-model="active" :safe-area-inset-bottom="true"> <van-tabbar route v-model="active" :safe-area-inset-bottom="true">
<van-tabbar-item to="/mobile/index" name="home" icon="home-o"> <van-tabbar-item to="/mobile/index" name="home" icon="home-o">
@@ -41,11 +47,23 @@
<script setup> <script setup>
import { useSharedStore } from '@/store/sharedata' import { useSharedStore } from '@/store/sharedata'
import { ref, watch } from 'vue' import { onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const active = ref('home') const active = ref('home')
const store = useSharedStore() const store = useSharedStore()
const theme = ref(store.theme) const theme = ref(store.theme)
const route = useRoute()
const router = useRouter()
const routerViewKey = ref(0)
// 监听路由变化,强制刷新组件
watch(
() => route.path,
() => {
routerViewKey.value += 1
}
)
watch( watch(
() => store.theme, () => store.theme,
@@ -53,18 +71,24 @@ watch(
theme.value = val theme.value = val
} }
) )
// 路由守卫
router.beforeEach((to, from, next) => {
// 可以在这里添加路由权限检查等逻辑
next()
})
onMounted(() => {
// 组件挂载时的初始化逻辑
})
</script> </script>
<style lang="scss"> <style lang="scss">
@use '../../assets/iconfont/iconfont.css' as *; @use '../../assets/iconfont/iconfont.css' as *;
.mobile-home { .mobile-home {
.container { .page-content {
.van-nav-bar { padding-bottom: 60px;
position: fixed;
width: 100%;
}
padding: 0 10px;
} }
.van-tabbar { .van-tabbar {
@@ -97,8 +121,25 @@ watch(
background: #1c1c1e; background: #1c1c1e;
} }
.van-nav-bar { // 路由切换动画
position: fixed; .move-enter-active,
width: 100%; .move-leave-active {
transition: all 0.3s ease;
}
.move-enter-from {
opacity: 0;
transform: translateX(100%);
}
.move-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.move-enter-to,
.move-leave-from {
opacity: 1;
transform: translateX(0);
} }
</style> </style>

View File

@@ -1,20 +1,22 @@
<template> <template>
<div class="mobile-image container"> <div class="mobile-image container">
<van-tabs v-model:active="activeName" class="my-tab" animated sticky> <CustomTabs :model-value="activeName" @update:model-value="activeName = $event" class="my-tab">
<van-tab title="MJ" name="mj" v-if="activeMenu.mj"> <CustomTabPane name="mj" label="MJ" v-if="activeMenu.mj">
<image-mj /> <image-mj />
</van-tab> </CustomTabPane>
<van-tab title="SD" name="sd" v-if="activeMenu.sd"> <CustomTabPane name="sd" label="SD" v-if="activeMenu.sd">
<image-sd /> <image-sd />
</van-tab> </CustomTabPane>
<van-tab title="DALL" name="dall" v-if="activeMenu.dall"> <CustomTabPane name="dall" label="DALL" v-if="activeMenu.dall">
<image-dall /> <image-dall />
</van-tab> </CustomTabPane>
</van-tabs> </CustomTabs>
</div> </div>
</template> </template>
<script setup> <script setup>
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
import CustomTabs from '@/components/ui/CustomTabs.vue'
import { httpGet } from '@/utils/http' import { httpGet } from '@/utils/http'
import ImageDall from '@/views/mobile/pages/ImageDall.vue' import ImageDall from '@/views/mobile/pages/ImageDall.vue'
import ImageMj from '@/views/mobile/pages/ImageMj.vue' import ImageMj from '@/views/mobile/pages/ImageMj.vue'

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="index container"> <div class="index">
<div class="header"> <div class="header">
<div class="user-greeting"> <div class="user-greeting px-3">
<div class="greeting-text"> <div class="greeting-text">
<h2 class="title">{{ getGreeting() }}</h2> <h2 class="title">{{ getGreeting() }}</h2>
<p class="subtitle">{{ title }}</p> <p class="subtitle">{{ title }}</p>
@@ -9,14 +9,14 @@
<div class="user-avatar" v-if="isLogin" @click="router.push('profile')"> <div class="user-avatar" v-if="isLogin" @click="router.push('profile')">
<van-image :src="userAvatar" round width="40" height="40" /> <van-image :src="userAvatar" round width="40" height="40" />
</div> </div>
<div class="login-btn" v-else @click="showLoginDialog(router)"> <div class="login-btn" v-else @click="router.push('login')">
<van-button size="small" type="primary" round>登录</van-button> <van-button size="small" type="primary" round>登录</van-button>
</div> </div>
</div> </div>
</div> </div>
<!-- 快捷操作区 --> <!-- 快捷操作区 -->
<div class="quick-actions mb-6"> <div class="quick-actions mb-6 px-3">
<van-row :gutter="12"> <van-row :gutter="12">
<van-col :span="12"> <van-col :span="12">
<div class="action-card primary" @click="router.push('chat')"> <div class="action-card primary" @click="router.push('chat')">
@@ -24,7 +24,7 @@
<i class="iconfont icon-chat action-icon"></i> <i class="iconfont icon-chat action-icon"></i>
<div class="action-text"> <div class="action-text">
<div class="action-title">AI 对话</div> <div class="action-title">AI 对话</div>
<div class="action-desc">智能助手随时待命</div> <div class="action-desc">智能助手对话</div>
</div> </div>
</div> </div>
</div> </div>
@@ -44,11 +44,11 @@
</div> </div>
<!-- 功能网格 --> <!-- 功能网格 -->
<div class="feature-section mb-6"> <div class="feature-section mb-6 px-3">
<div class="section-header"> <div class="section-header">
<h3 class="section-title">AI 功能</h3> <h3 class="section-title">AI 功能</h3>
</div> </div>
<van-grid :column-num="4" :gutter="12" :border="false"> <van-grid :column-num="4" :border="false">
<van-grid-item <van-grid-item
v-for="feature in features" v-for="feature in features"
:key="feature.key" :key="feature.key"
@@ -68,7 +68,7 @@
</div> </div>
<!-- 推荐应用 --> <!-- 推荐应用 -->
<div class="apps-section"> <div class="apps-section px-3">
<div class="section-header"> <div class="section-header">
<h3 class="section-title">推荐应用</h3> <h3 class="section-title">推荐应用</h3>
<van-button <van-button
@@ -86,14 +86,9 @@
<div class="app-list"> <div class="app-list">
<van-swipe :autoplay="3000" :show-indicators="false" class="app-swipe"> <van-swipe :autoplay="3000" :show-indicators="false" class="app-swipe">
<van-swipe-item v-for="chunk in appChunks" :key="chunk[0]?.id"> <van-swipe-item v-for="chunk in appChunks" :key="chunk[0] && chunk[0].id">
<div class="app-row"> <div class="app-row px-3">
<div <div v-for="item in chunk" :key="item.id" class="app-item" @click="useRole(item.id)">
v-for="item in chunk"
:key="item.id"
class="app-item"
@click="useRole(item.id)"
>
<div class="app-avatar"> <div class="app-avatar">
<van-image :src="item.icon" round fit="cover" /> <van-image :src="item.icon" round fit="cover" />
</div> </div>
@@ -102,7 +97,7 @@
<div class="app-desc">{{ item.intro }}</div> <div class="app-desc">{{ item.intro }}</div>
</div> </div>
<div class="app-action"> <div class="app-action">
<van-button <!-- <van-button
size="mini" size="mini"
type="primary" type="primary"
plain plain
@@ -110,7 +105,9 @@
@click.stop="updateRole(item, hasRole(item.key) ? 'remove' : 'add')" @click.stop="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
> >
{{ hasRole(item.key) ? '已添加' : '添加' }} {{ hasRole(item.key) ? '已添加' : '添加' }}
</van-button> </van-button> -->
<van-button size="small" type="primary" round> 开始对话 </van-button>
</div> </div>
</div> </div>
</div> </div>
@@ -118,33 +115,6 @@
</van-swipe> </van-swipe>
</div> </div>
</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> </div>
</template> </template>
@@ -164,30 +134,61 @@ const apps = ref([])
const loading = ref(false) const loading = ref(false)
const roles = ref([]) const roles = ref([])
const userAvatar = ref('/images/avatar/default.jpg') const userAvatar = ref('/images/avatar/default.jpg')
const userStats = ref({
conversations: 0,
images: 0,
power: 0
})
// 功能配置 // 功能配置
const features = ref([ const features = ref([
{ key: 'mj', name: 'MJ绘画', icon: 'icon-mj', color: '#8B5CF6', url: '/mobile/create?tab=mj' }, { 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: '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: 'dalle',
{ key: 'video', name: '视频生成', icon: 'icon-video', color: '#10B981', url: '/mobile/create?tab=video' }, name: 'DALL·E',
{ key: 'jimeng', name: '即梦AI', icon: 'icon-jimeng', color: '#F97316', url: '/mobile/create?tab=jimeng' }, icon: 'icon-dalle',
{ key: 'xmind', name: '思维导图', icon: 'icon-mind', color: '#3B82F6', url: '/mobile/tools?tab=xmind' }, color: '#F59E0B',
{ key: 'imgWall', name: '作品展示', icon: 'icon-gallery', color: '#EC4899', url: '/mobile/imgWall' } 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个 // 应用分组显示每行2个
const appChunks = computed(() => { const appChunks = computed(() => {
const chunks = [] const chunks = []
const displayApps = apps.value.slice(0, 6) // 只显示前6个 const displayApps = apps.value.slice(0, 12) // 只显示前6个
for (let i = 0; i < displayApps.length; i += 2) { for (let i = 0; i < displayApps.length; i += 4) {
chunks.push(displayApps.slice(i, i + 2)) chunks.push(displayApps.slice(i, i + 4))
} }
return chunks return chunks
}) })
@@ -220,8 +221,6 @@ onMounted(() => {
isLogin.value = true isLogin.value = true
roles.value = user.chat_roles roles.value = user.chat_roles
userAvatar.value = user.avatar || '/images/avatar/default.jpg' userAvatar.value = user.avatar || '/images/avatar/default.jpg'
// 获取用户统计数据
fetchUserStats()
}) })
.catch(() => {}) .catch(() => {})
@@ -243,37 +242,21 @@ 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) => { const updateRole = (row, opt) => {
if (!isLogin.value) { if (!isLogin.value) {
return showLoginDialog(router) return showLoginDialog(router)
} }
const title = ref('') let actionTitle = ''
if (opt === 'add') { if (opt === 'add') {
title.value = '添加应用' actionTitle = '添加应用'
const exists = arrayContains(roles.value, row.key) const exists = arrayContains(roles.value, row.key)
if (exists) { if (exists) {
return return
} }
roles.value.push(row.key) roles.value.push(row.key)
} else { } else {
title.value = '移除应用' actionTitle = '移除应用'
const exists = arrayContains(roles.value, row.key) const exists = arrayContains(roles.value, row.key)
if (!exists) { if (!exists) {
return return
@@ -282,10 +265,10 @@ const updateRole = (row, opt) => {
} }
httpPost('/api/app/update', { keys: roles.value }) httpPost('/api/app/update', { keys: roles.value })
.then(() => { .then(() => {
showNotify({ type: 'success', message: title.value + '成功!', duration: 1000 }) showNotify({ type: 'success', message: actionTitle + '成功!', duration: 1000 })
}) })
.catch((e) => { .catch((e) => {
showNotify({ type: 'danger', message: title.value + '失败:' + e.message }) showNotify({ type: 'danger', message: actionTitle + '失败:' + e.message })
}) })
} }
@@ -306,8 +289,7 @@ const useRole = (roleId) => {
color: var(--van-text-color); color: var(--van-text-color);
background: linear-gradient(135deg, var(--van-background), var(--van-background-2)); background: linear-gradient(135deg, var(--van-background), var(--van-background-2));
min-height: 100vh; min-height: 100vh;
padding: 0 16px 60px; padding: 0;
.header { .header {
padding: 20px 0 16px; padding: 20px 0 16px;
position: sticky; position: sticky;
@@ -329,7 +311,7 @@ const useRole = (roleId) => {
font-weight: 700; font-weight: 700;
color: var(--van-text-color); color: var(--van-text-color);
margin: 0 0 4px 0; margin: 0 0 4px 0;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6); background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@@ -364,7 +346,7 @@ const useRole = (roleId) => {
} }
&.primary { &.primary {
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6); background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
color: white; color: white;
.action-icon, .action-icon,
@@ -375,7 +357,7 @@ const useRole = (roleId) => {
} }
&.secondary { &.secondary {
background: linear-gradient(135deg, #06B6D4, #10B981); background: linear-gradient(135deg, #06b6d4, #10b981);
color: white; color: white;
.action-icon, .action-icon,
@@ -412,8 +394,7 @@ const useRole = (roleId) => {
} }
.feature-section, .feature-section,
.apps-section, .apps-section {
.stats-section {
.section-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -535,28 +516,6 @@ const useRole = (roleId) => {
} }
} }
} }
.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);
}
}
}
} }
// 响应式调整 // 响应式调整
@@ -593,8 +552,7 @@ const useRole = (roleId) => {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} }
.apps-section .app-swipe .app-row .app-item, .apps-section .app-swipe .app-row .app-item {
.stats-section .stat-card {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
} }
} }

View File

@@ -1,7 +1,5 @@
<template> <template>
<div class="invite-page"> <div class="invite-page">
<van-nav-bar title="邀请好友" left-arrow @click-left="router.back()" fixed />
<div class="invite-content"> <div class="invite-content">
<!-- 邀请头图 --> <!-- 邀请头图 -->
<div class="invite-header"> <div class="invite-header">
@@ -100,11 +98,7 @@
</div> </div>
<div class="code-value">{{ inviteCode }}</div> <div class="code-value">{{ inviteCode }}</div>
<div class="code-link"> <div class="code-link">
<van-field <van-field v-model="inviteLink" readonly placeholder="邀请链接">
v-model="inviteLink"
readonly
placeholder="邀请链接"
>
<template #button> <template #button>
<van-button size="small" type="primary" @click="copyInviteLink"> <van-button size="small" type="primary" @click="copyInviteLink">
复制链接 复制链接
@@ -131,11 +125,7 @@
finished-text="没有更多记录" finished-text="没有更多记录"
@load="loadInviteRecords" @load="loadInviteRecords"
> >
<div <div v-for="record in displayRecords" :key="record.id" class="record-item">
v-for="record in displayRecords"
:key="record.id"
class="record-item"
>
<div class="record-avatar"> <div class="record-avatar">
<van-image :src="record.avatar" round width="40" height="40" /> <van-image :src="record.avatar" round width="40" height="40" />
</div> </div>
@@ -150,7 +140,10 @@
</div> </div>
</div> </div>
<van-empty v-if="!recordsLoading && inviteRecords.length === 0" description="暂无邀请记录" /> <van-empty
v-if="!recordsLoading && inviteRecords.length === 0"
description="暂无邀请记录"
/>
</van-list> </van-list>
</div> </div>
</div> </div>
@@ -180,9 +173,8 @@
<script setup> <script setup>
import { checkSession } from '@/store/cache' import { checkSession } from '@/store/cache'
import { httpGet } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs' import { showLoginDialog } from '@/utils/libs'
import { showFailToast, showNotify, showSuccessToast } from 'vant' import { showNotify, showSuccessToast } from 'vant'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -190,7 +182,7 @@ const router = useRouter()
const userStats = ref({ const userStats = ref({
inviteCount: 0, inviteCount: 0,
rewardTotal: 0, rewardTotal: 0,
todayInvite: 0 todayInvite: 0,
}) })
const inviteCode = ref('') const inviteCode = ref('')
const inviteLink = ref('') const inviteLink = ref('')
@@ -209,7 +201,7 @@ const rewardRules = ref([
desc: '好友通过邀请链接成功注册', desc: '好友通过邀请链接成功注册',
icon: 'icon-user-plus', icon: 'icon-user-plus',
color: '#1989fa', color: '#1989fa',
reward: 50 reward: 50,
}, },
{ {
id: 2, id: 2,
@@ -217,7 +209,7 @@ const rewardRules = ref([
desc: '好友首次充值任意金额', desc: '好友首次充值任意金额',
icon: 'icon-money', icon: 'icon-money',
color: '#07c160', color: '#07c160',
reward: 100 reward: 100,
}, },
{ {
id: 3, id: 3,
@@ -225,8 +217,8 @@ const rewardRules = ref([
desc: '好友连续使用7天', desc: '好友连续使用7天',
icon: 'icon-star', icon: 'icon-star',
color: '#ff9500', color: '#ff9500',
reward: 200 reward: 200,
} },
]) ])
// 显示的记录根据showAllRecords决定 // 显示的记录根据showAllRecords决定
@@ -251,7 +243,6 @@ const initPage = async () => {
// 加载邀请记录 // 加载邀请记录
loadInviteRecords() loadInviteRecords()
} catch (error) { } catch (error) {
showLoginDialog(router) showLoginDialog(router)
} }
@@ -271,7 +262,7 @@ const fetchInviteStats = () => {
userStats.value = { userStats.value = {
inviteCount: Math.floor(Math.random() * 50), inviteCount: Math.floor(Math.random() * 50),
rewardTotal: Math.floor(Math.random() * 5000), rewardTotal: Math.floor(Math.random() * 5000),
todayInvite: Math.floor(Math.random() * 5) todayInvite: Math.floor(Math.random() * 5),
} }
} }
@@ -304,7 +295,7 @@ const generateMockRecords = () => {
username: names[i % names.length] + (i + 1), username: names[i % names.length] + (i + 1),
avatar: '/images/avatar/default.jpg', avatar: '/images/avatar/default.jpg',
status: Math.random() > 0.3 ? 'completed' : 'pending', status: Math.random() > 0.3 ? 'completed' : 'pending',
created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString() created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
}) })
} }
@@ -324,7 +315,7 @@ const shareToWeChat = () => {
title: '邀请你使用AI创作平台', title: '邀请你使用AI创作平台',
desc: '强大的AI工具让创作更简单', desc: '强大的AI工具让创作更简单',
link: inviteLink.value, link: inviteLink.value,
imgUrl: `${location.origin}/images/share-logo.png` imgUrl: `${location.origin}/images/share-logo.png`,
}) })
} else { } else {
// 复制链接提示 // 复制链接提示
@@ -383,7 +374,7 @@ const shareToFriends = () => {
navigator.share({ navigator.share({
title: '邀请你使用AI创作平台', title: '邀请你使用AI创作平台',
text: '强大的AI工具让创作更简单', text: '强大的AI工具让创作更简单',
url: inviteLink.value url: inviteLink.value,
}) })
} else { } else {
copyInviteLink() copyInviteLink()
@@ -408,7 +399,7 @@ const onImageError = (e) => {
position: relative; position: relative;
height: 200px; height: 200px;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6); background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
.header-bg { .header-bg {
position: absolute; position: absolute;
@@ -588,7 +579,7 @@ const onImageError = (e) => {
} }
&.qr { &.qr {
background: #8B5CF6; background: #8b5cf6;
} }
&.more { &.more {

View File

@@ -1,5 +1,13 @@
<template> <template>
<div class="login flex w-full flex-col place-content-center h-lvh"> <div class="login flex w-full flex-col place-content-center h-lvh">
<!-- 返回首页链接 -->
<div class="back-home">
<el-button @click="goHome" type="text" class="back-btn">
<i class="iconfont icon-home"></i>
返回首页
</el-button>
</div>
<el-image :src="logo" class="w-1/2 mx-auto logo" /> <el-image :src="logo" class="w-1/2 mx-auto logo" />
<div class="title text-center text-3xl font-bold mt-8">{{ title }}</div> <div class="title text-center text-3xl font-bold mt-8">{{ title }}</div>
<div class="w-full p-8"> <div class="w-full p-8">
@@ -22,6 +30,10 @@ const loginSuccess = () => {
router.back() router.back()
} }
const goHome = () => {
router.push('/mobile/index')
}
onMounted(() => { onMounted(() => {
getSystemInfo().then((res) => { getSystemInfo().then((res) => {
title.value = res.data.title title.value = res.data.title
@@ -34,6 +46,23 @@ onMounted(() => {
.login { .login {
background: var(--theme-bg); background: var(--theme-bg);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative;
.back-home {
position: absolute;
top: 20px;
left: 20px;
z-index: 10;
.back-btn {
color: var(--text-theme-color);
font-size: 14px;
.iconfont {
margin-right: 4px;
}
}
}
.logo { .logo {
background: #ffffff; background: #ffffff;

View File

@@ -1,11 +1,5 @@
<template> <template>
<div class="member-center"> <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="member-content">
<!-- 用户信息卡片 --> <!-- 用户信息卡片 -->
<div class="user-card" v-if="isLogin"> <div class="user-card" v-if="isLogin">

View File

@@ -1,11 +1,5 @@
<template> <template>
<div class="power-log"> <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="power-content">
<!-- 统计概览 --> <!-- 统计概览 -->
<div class="stats-overview"> <div class="stats-overview">
@@ -35,20 +29,29 @@
<!-- 筛选栏 --> <!-- 筛选栏 -->
<div class="filter-bar"> <div class="filter-bar">
<van-tabs v-model:active="activeType" @change="onTypeChange" shrink> <CustomTabs
<van-tab title="全部" name="all" /> :model-value="activeType"
<van-tab title="对话" name="chat" /> @update:model-value="activeType = $event"
<van-tab title="绘画" name="image" /> @tab-click="onTypeChange"
<van-tab title="音乐" name="music" /> >
<van-tab title="视频" name="video" /> <CustomTabPane name="all" label="全部" />
</van-tabs> <CustomTabPane name="chat" label="对话" />
<CustomTabPane name="image" label="绘画" />
<CustomTabPane name="music" label="音乐" />
<CustomTabPane name="video" label="视频" />
</CustomTabs>
</div> </div>
<!-- 日志列表 --> <!-- 日志列表 -->
<div class="log-list"> <div class="log-list">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh
:model-value="refreshing"
@update:model-value="refreshing = $event"
@refresh="onRefresh"
>
<van-list <van-list
v-model:loading="loading" :model-value="loading"
@update:model-value="loading = $event"
:finished="finished" :finished="finished"
finished-text="没有更多了" finished-text="没有更多了"
@load="onLoad" @load="onLoad"
@@ -80,13 +83,21 @@
</div> </div>
<!-- 筛选弹窗 --> <!-- 筛选弹窗 -->
<van-action-sheet v-model:show="showFilter" title="筛选条件"> <van-action-sheet
:model-value="showFilter"
@update:model-value="showFilter = $event"
title="筛选条件"
>
<div class="filter-content"> <div class="filter-content">
<van-form> <van-form>
<van-field label="时间范围"> <van-field label="时间范围">
<template #input> <template #input>
<van-button size="small" @click="showDatePicker = true"> <van-button size="small" @click="showDatePicker = true">
{{ dateRange.start && dateRange.end ? `${dateRange.start} ${dateRange.end}` : '选择时间' }} {{
dateRange.start && dateRange.end
? `${dateRange.start} ${dateRange.end}`
: '选择时间'
}}
</van-button> </van-button>
</template> </template>
</van-field> </van-field>
@@ -110,7 +121,8 @@
<!-- 日期选择器 --> <!-- 日期选择器 -->
<van-calendar <van-calendar
v-model:show="showDatePicker" :model-value="showDatePicker"
@update:model-value="showDatePicker = $event"
type="range" type="range"
@confirm="onDateConfirm" @confirm="onDateConfirm"
:max-date="new Date()" :max-date="new Date()"
@@ -119,8 +131,8 @@
</template> </template>
<script setup> <script setup>
import { httpGet } from '@/utils/http' import CustomTabPane from '@/components/ui/CustomTabPane.vue'
import { showFailToast } from 'vant' import CustomTabs from '@/components/ui/CustomTabs.vue'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -135,21 +147,21 @@ const showDatePicker = ref(false)
const filterType = ref('all') const filterType = ref('all')
const dateRange = ref({ const dateRange = ref({
start: '', start: '',
end: '' end: '',
}) })
// 统计数据 // 统计数据
const stats = ref({ const stats = ref({
total: 0, total: 0,
today: 0, today: 0,
balance: 0 balance: 0,
}) })
// 分页参数 // 分页参数
const pageParams = ref({ const pageParams = ref({
page: 1, page: 1,
limit: 20, limit: 20,
type: 'all' type: 'all',
}) })
onMounted(() => { onMounted(() => {
@@ -168,7 +180,7 @@ const fetchStats = () => {
stats.value = { stats.value = {
total: Math.floor(Math.random() * 10000), total: Math.floor(Math.random() * 10000),
today: Math.floor(Math.random() * 100), today: Math.floor(Math.random() * 100),
balance: Math.floor(Math.random() * 1000) balance: Math.floor(Math.random() * 1000),
} }
} }
@@ -227,7 +239,7 @@ const generateMockData = (page, limit) => {
chat: ['GPT-4对话', 'Claude对话', '智能助手'], chat: ['GPT-4对话', 'Claude对话', '智能助手'],
image: ['MidJourney生成', 'Stable Diffusion', 'DALL-E创作'], image: ['MidJourney生成', 'Stable Diffusion', 'DALL-E创作'],
music: ['Suno音乐创作', '音频生成'], music: ['Suno音乐创作', '音频生成'],
video: ['视频生成', 'Luma创作'] video: ['视频生成', 'Luma创作'],
} }
const data = [] const data = []
@@ -249,7 +261,7 @@ const generateMockData = (page, limit) => {
title, title,
cost: Math.floor(Math.random() * 50) + 1, cost: Math.floor(Math.random() * 50) + 1,
remark: Math.random() > 0.5 ? '消费详情使用高级模型进行AI创作效果优质' : '', remark: Math.random() > 0.5 ? '消费详情使用高级模型进行AI创作效果优质' : '',
created_at: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString() created_at: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(),
}) })
} }
@@ -262,7 +274,7 @@ const getTypeIcon = (type) => {
chat: 'icon-chat', chat: 'icon-chat',
image: 'icon-mj', image: 'icon-mj',
music: 'icon-music', music: 'icon-music',
video: 'icon-video' video: 'icon-video',
} }
return icons[type] || 'icon-chat' return icons[type] || 'icon-chat'
} }
@@ -273,7 +285,7 @@ const getTypeColor = (type) => {
chat: '#1989fa', chat: '#1989fa',
image: '#8B5CF6', image: '#8B5CF6',
music: '#ee0a24', music: '#ee0a24',
video: '#07c160' video: '#07c160',
} }
return colors[type] || '#1989fa' return colors[type] || '#1989fa'
} }
@@ -284,11 +296,14 @@ const formatTime = (timeStr) => {
const now = new Date() const now = new Date()
const diff = now - date const diff = now - date
if (diff < 60000) { // 1分钟内 if (diff < 60000) {
// 1分钟内
return '刚刚' return '刚刚'
} else if (diff < 3600000) { // 1小时内 } else if (diff < 3600000) {
// 1小时内
return `${Math.floor(diff / 60000)}分钟前` return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 24小时内 } else if (diff < 86400000) {
// 24小时内
return `${Math.floor(diff / 3600000)}小时前` return `${Math.floor(diff / 3600000)}小时前`
} else { } else {
return date.toLocaleDateString() return date.toLocaleDateString()
@@ -300,7 +315,7 @@ const onDateConfirm = (values) => {
const [start, end] = values const [start, end] = values
dateRange.value = { dateRange.value = {
start: start.toLocaleDateString(), start: start.toLocaleDateString(),
end: end.toLocaleDateString() end: end.toLocaleDateString(),
} }
showDatePicker.value = false showDatePicker.value = false
} }
@@ -333,7 +348,7 @@ const applyFilter = () => {
.stats-overview { .stats-overview {
padding: 16px; padding: 16px;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6); background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
.stats-card { .stats-card {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);

View File

@@ -147,32 +147,17 @@
</van-cell-group> </van-cell-group>
<van-cell-group inset> <van-cell-group inset>
<van-cell <van-cell title="帮助中心" icon="question-o" is-link @click="router.push('/mobile/help')">
title="帮助中心"
icon="question-o"
is-link
@click="router.push('/mobile/help')"
>
<template #icon> <template #icon>
<i class="iconfont icon-help menu-icon"></i> <i class="iconfont icon-help menu-icon"></i>
</template> </template>
</van-cell> </van-cell>
<van-cell <van-cell title="意见反馈" icon="chat-o" is-link @click="router.push('/mobile/feedback')">
title="意见反馈"
icon="chat-o"
is-link
@click="router.push('/mobile/feedback')"
>
<template #icon> <template #icon>
<i class="iconfont icon-message menu-icon"></i> <i class="iconfont icon-message menu-icon"></i>
</template> </template>
</van-cell> </van-cell>
<van-cell <van-cell title="关于我们" icon="info-o" is-link @click="showAbout = true">
title="关于我们"
icon="info-o"
is-link
@click="showAbout = true"
>
<template #icon> <template #icon>
<i class="iconfont icon-info menu-icon"></i> <i class="iconfont icon-info menu-icon"></i>
</template> </template>
@@ -182,13 +167,7 @@
<!-- 退出登录 --> <!-- 退出登录 -->
<div class="logout-section" v-if="isLogin"> <div class="logout-section" v-if="isLogin">
<van-button <van-button size="large" block type="danger" plain @click="showLogoutConfirm = true">
size="large"
block
type="danger"
plain
@click="showLogoutConfirm = true"
>
退出登录 退出登录
</van-button> </van-button>
</div> </div>
@@ -198,11 +177,15 @@
<p class="app-version">版本 v{{ appVersion }}</p> <p class="app-version">版本 v{{ appVersion }}</p>
<p class="copyright">© 2024 {{ title }}. All rights reserved.</p> <p class="copyright">© 2024 {{ title }}. All rights reserved.</p>
</div> </div>
<!-- 底部安全间距 -->
<div class="bottom-safe-area"></div>
</div> </div>
<!-- 修改密码弹窗 --> <!-- 修改密码弹窗 -->
<van-dialog <van-dialog
v-model:show="showPasswordDialog" :model-value="showPasswordDialog"
@update:model-value="showPasswordDialog = $event"
title="修改密码" title="修改密码"
show-cancel-button show-cancel-button
@confirm="updatePass" @confirm="updatePass"
@@ -234,7 +217,7 @@
required required
:rules="[ :rules="[
{ required: true, message: '请再次输入新密码' }, { required: true, message: '请再次输入新密码' },
{ validator: validateConfirmPassword } { validator: validateConfirmPassword },
]" ]"
/> />
</van-cell-group> </van-cell-group>
@@ -242,7 +225,11 @@
</van-dialog> </van-dialog>
<!-- 设置弹窗 --> <!-- 设置弹窗 -->
<van-action-sheet v-model:show="showSettings" title="设置"> <van-action-sheet
:model-value="showSettings"
@update:model-value="showSettings = $event"
title="设置"
>
<div class="settings-content"> <div class="settings-content">
<van-cell-group> <van-cell-group>
<van-cell title="暗黑主题"> <van-cell title="暗黑主题">
@@ -255,10 +242,7 @@
</van-cell> </van-cell>
<van-cell title="流式输出"> <van-cell title="流式输出">
<template #right-icon> <template #right-icon>
<van-switch <van-switch v-model="stream" @change="(val) => store.setChatStream(val)" />
v-model="stream"
@change="(val) => store.setChatStream(val)"
/>
</template> </template>
</van-cell> </van-cell>
<van-cell title="消息通知"> <van-cell title="消息通知">
@@ -276,7 +260,11 @@
</van-action-sheet> </van-action-sheet>
<!-- 头像选择弹窗 --> <!-- 头像选择弹窗 -->
<van-action-sheet v-model:show="showAvatarOptions" title="更换头像"> <van-action-sheet
:model-value="showAvatarOptions"
@update:model-value="showAvatarOptions = $event"
title="更换头像"
>
<div class="avatar-options"> <div class="avatar-options">
<van-cell title="拍照" icon="photograph" @click="selectAvatar('camera')" /> <van-cell title="拍照" icon="photograph" @click="selectAvatar('camera')" />
<van-cell title="从相册选择" icon="photo-o" @click="selectAvatar('album')" /> <van-cell title="从相册选择" icon="photo-o" @click="selectAvatar('album')" />
@@ -285,7 +273,12 @@
</van-action-sheet> </van-action-sheet>
<!-- 关于我们弹窗 --> <!-- 关于我们弹窗 -->
<van-dialog v-model:show="showAbout" title="关于我们" :show-cancel-button="false"> <van-dialog
:model-value="showAbout"
@update:model-value="showAbout = $event"
title="关于我们"
:show-cancel-button="false"
>
<div class="about-content"> <div class="about-content">
<div class="about-logo"> <div class="about-logo">
<img src="/images/logo.png" alt="Logo" /> <img src="/images/logo.png" alt="Logo" />
@@ -303,7 +296,8 @@
<!-- 退出登录确认 --> <!-- 退出登录确认 -->
<van-dialog <van-dialog
v-model:show="showLogoutConfirm" :model-value="showLogoutConfirm"
@update:model-value="showLogoutConfirm = $event"
title="退出登录" title="退出登录"
message="确定要退出登录吗?" message="确定要退出登录吗?"
show-cancel-button show-cancel-button
@@ -317,8 +311,7 @@ import { checkSession, getSystemInfo } from '@/store/cache'
import { removeUserToken } from '@/store/session' import { removeUserToken } from '@/store/session'
import { useSharedStore } from '@/store/sharedata' import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http' import { httpGet, httpPost } from '@/utils/http'
import { dateFormat, showLoginDialog } from '@/utils/libs' import { showLoginDialog } from '@/utils/libs'
import { ElMessage } from 'element-plus'
import { showFailToast, showLoadingToast, showNotify, showSuccessToast } from 'vant' import { showFailToast, showLoadingToast, showNotify, showSuccessToast } from 'vant'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -369,7 +362,7 @@ const pass = ref({
// 密码验证规则 // 密码验证规则
const passwordRules = [ const passwordRules = [
{ required: true, message: '请输入新密码' }, { required: true, message: '请输入新密码' },
{ min: 8, max: 16, message: '密码长度为8-16个字符' } { min: 8, max: 16, message: '密码长度为8-16个字符' },
] ]
// 计算属性 // 计算属性
@@ -455,11 +448,14 @@ const updatePass = () => {
return return
} }
passwordForm.value.validate().then(() => { passwordForm.value
updatePasswordAPI() .validate()
}).catch((errors) => { .then(() => {
console.log('表单验证失败:', errors) updatePasswordAPI()
}) })
.catch((errors) => {
console.log('表单验证失败:', errors)
})
} }
const updatePasswordAPI = () => { const updatePasswordAPI = () => {
@@ -570,12 +566,13 @@ const logout = function () {
.profile-page { .profile-page {
min-height: 100vh; min-height: 100vh;
background: var(--van-background); background: var(--van-background);
padding-bottom: 60px;
.profile-header { .profile-header {
position: relative; position: relative;
height: 240px; height: 240px;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6); background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
.header-bg { .header-bg {
position: absolute; position: absolute;
@@ -652,10 +649,9 @@ const logout = function () {
} }
.profile-content { .profile-content {
margin-top: -30px; margin-top: 20px;
position: relative;
z-index: 3; z-index: 3;
padding: 0 16px 60px; padding: 0 16px 20px;
.status-cards { .status-cards {
margin-bottom: 24px; margin-bottom: 24px;
@@ -760,7 +756,7 @@ const logout = function () {
} }
&.share { &.share {
background: linear-gradient(135deg, #8B5CF6, #7c3aed); background: linear-gradient(135deg, #8b5cf6, #7c3aed);
} }
&.settings { &.settings {
@@ -824,6 +820,11 @@ const logout = function () {
margin: 0; margin: 0;
} }
} }
.bottom-safe-area {
height: 20px;
width: 100%;
}
} }
// 弹窗样式 // 弹窗样式
@@ -923,7 +924,7 @@ const logout = function () {
} }
.profile-content { .profile-content {
padding: 0 12px 60px; padding: 0 12px 20px;
.status-cards .status-card { .status-cards .status-card {
padding: 12px; padding: 12px;

View File

@@ -1,7 +1,5 @@
<template> <template>
<div class="settings-page"> <div class="settings-page">
<van-nav-bar title="设置" left-arrow @click-left="router.back()" fixed />
<div class="settings-content"> <div class="settings-content">
<!-- 个人设置 --> <!-- 个人设置 -->
<div class="setting-section"> <div class="setting-section">
@@ -39,10 +37,7 @@
<i class="iconfont icon-moon setting-icon"></i> <i class="iconfont icon-moon setting-icon"></i>
</template> </template>
<template #right-icon> <template #right-icon>
<van-switch <van-switch v-model="darkMode" @change="onThemeChange" />
v-model="darkMode"
@change="onThemeChange"
/>
</template> </template>
</van-cell> </van-cell>
<van-cell title="流式输出"> <van-cell title="流式输出">
@@ -50,10 +45,7 @@
<i class="iconfont icon-stream setting-icon"></i> <i class="iconfont icon-stream setting-icon"></i>
</template> </template>
<template #right-icon> <template #right-icon>
<van-switch <van-switch v-model="streamOutput" @change="onStreamChange" />
v-model="streamOutput"
@change="onStreamChange"
/>
</template> </template>
</van-cell> </van-cell>
<van-cell title="消息通知"> <van-cell title="消息通知">
@@ -217,11 +209,7 @@
@click="selectLanguage(lang)" @click="selectLanguage(lang)"
> >
<template #right-icon> <template #right-icon>
<van-icon <van-icon v-if="currentLanguage.code === lang.code" name="success" color="#07c160" />
v-if="currentLanguage.code === lang.code"
name="success"
color="#07c160"
/>
</template> </template>
</van-cell> </van-cell>
</div> </div>
@@ -239,11 +227,7 @@
@click="selectModel(model)" @click="selectModel(model)"
> >
<template #right-icon> <template #right-icon>
<van-icon <van-icon v-if="currentModel.code === model.code" name="success" color="#07c160" />
v-if="currentModel.code === model.code"
name="success"
color="#07c160"
/>
</template> </template>
</van-cell> </van-cell>
</div> </div>
@@ -261,11 +245,7 @@
@click="selectSendMode(mode)" @click="selectSendMode(mode)"
> >
<template #right-icon> <template #right-icon>
<van-icon <van-icon v-if="currentSendMode.code === mode.code" name="success" color="#07c160" />
v-if="currentSendMode.code === mode.code"
name="success"
color="#07c160"
/>
</template> </template>
</van-cell> </van-cell>
</div> </div>
@@ -346,7 +326,7 @@ const showAbout = ref(false)
const passwordForm = ref({ const passwordForm = ref({
old: '', old: '',
new: '', new: '',
confirm: '' confirm: '',
}) })
// 语言选项 // 语言选项
@@ -355,7 +335,7 @@ const languages = ref([
{ code: 'zh-TW', name: '繁體中文' }, { code: 'zh-TW', name: '繁體中文' },
{ code: 'en', name: 'English' }, { code: 'en', name: 'English' },
{ code: 'ja', name: '日本語' }, { code: 'ja', name: '日本語' },
{ code: 'ko', name: '한국어' } { code: 'ko', name: '한국어' },
]) ])
const currentLanguage = ref(languages.value[0]) const currentLanguage = ref(languages.value[0])
@@ -365,7 +345,7 @@ const models = ref([
{ code: 'gpt-4', name: 'GPT-4', desc: '最新的GPT-4模型性能强大' }, { code: 'gpt-4', name: 'GPT-4', desc: '最新的GPT-4模型性能强大' },
{ code: 'gpt-3.5', name: 'GPT-3.5', desc: '经典的GPT-3.5模型,速度快' }, { code: 'gpt-3.5', name: 'GPT-3.5', desc: '经典的GPT-3.5模型,速度快' },
{ code: 'claude', name: 'Claude', desc: '人工智能助手Claude' }, { code: 'claude', name: 'Claude', desc: '人工智能助手Claude' },
{ code: 'gemini', name: 'Gemini', desc: 'Google的Gemini模型' } { code: 'gemini', name: 'Gemini', desc: 'Google的Gemini模型' },
]) ])
const currentModel = ref(models.value[0]) const currentModel = ref(models.value[0])
@@ -374,7 +354,7 @@ const currentModel = ref(models.value[0])
const sendModes = ref([ const sendModes = ref([
{ code: 'enter', name: 'Enter发送', desc: '按Enter键发送消息' }, { code: 'enter', name: 'Enter发送', desc: '按Enter键发送消息' },
{ code: 'ctrl+enter', name: 'Ctrl+Enter发送', desc: '按Ctrl+Enter发送消息' }, { code: 'ctrl+enter', name: 'Ctrl+Enter发送', desc: '按Ctrl+Enter发送消息' },
{ code: 'button', name: '仅按钮发送', desc: '只能点击发送按钮' } { code: 'button', name: '仅按钮发送', desc: '只能点击发送按钮' },
]) ])
const currentSendMode = ref(sendModes.value[0]) const currentSendMode = ref(sendModes.value[0])
@@ -389,26 +369,26 @@ const loadSettings = () => {
const savedSettings = localStorage.getItem('app-settings') const savedSettings = localStorage.getItem('app-settings')
if (savedSettings) { if (savedSettings) {
const settings = JSON.parse(savedSettings) const settings = JSON.parse(savedSettings)
darkMode.value = settings.darkMode ?? (store.theme === 'dark') darkMode.value = settings.darkMode ?? store.theme === 'dark'
streamOutput.value = settings.streamOutput ?? store.chatStream streamOutput.value = settings.streamOutput ?? store.chatStream
notifications.value = settings.notifications ?? true notifications.value = settings.notifications ?? true
autoSave.value = settings.autoSave ?? true autoSave.value = settings.autoSave ?? true
saveHistory.value = settings.saveHistory ?? true saveHistory.value = settings.saveHistory ?? true
// 恢复语言设置 // 恢复语言设置
const savedLang = languages.value.find(lang => lang.code === settings.language) const savedLang = languages.value.find((lang) => lang.code === settings.language)
if (savedLang) { if (savedLang) {
currentLanguage.value = savedLang currentLanguage.value = savedLang
} }
// 恢复模型设置 // 恢复模型设置
const savedModel = models.value.find(model => model.code === settings.model) const savedModel = models.value.find((model) => model.code === settings.model)
if (savedModel) { if (savedModel) {
currentModel.value = savedModel currentModel.value = savedModel
} }
// 恢复发送方式设置 // 恢复发送方式设置
const savedSendMode = sendModes.value.find(mode => mode.code === settings.sendMode) const savedSendMode = sendModes.value.find((mode) => mode.code === settings.sendMode)
if (savedSendMode) { if (savedSendMode) {
currentSendMode.value = savedSendMode currentSendMode.value = savedSendMode
} }
@@ -425,7 +405,7 @@ const saveSettings = () => {
saveHistory: saveHistory.value, saveHistory: saveHistory.value,
language: currentLanguage.value.code, language: currentLanguage.value.code,
model: currentModel.value.code, model: currentModel.value.code,
sendMode: currentSendMode.value.code sendMode: currentSendMode.value.code,
} }
localStorage.setItem('app-settings', JSON.stringify(settings)) localStorage.setItem('app-settings', JSON.stringify(settings))
} }

View File

@@ -1,16 +1,18 @@
<template> <template>
<div class="tools-page"> <div class="tools-page">
<van-nav-bar title="AI 工具" left-arrow @click-left="router.back()" fixed />
<div class="tools-content"> <div class="tools-content">
<!-- 工具分类 --> <!-- 工具分类 -->
<van-tabs v-model:active="activeCategory" @change="onCategoryChange" sticky :offset-top="46"> <CustomTabs
<van-tab title="全部" name="all" /> :model-value="activeCategory"
<van-tab title="办公工具" name="office" /> @update:model-value="activeCategory = $event"
<van-tab title="创意工具" name="creative" /> @tab-click="onCategoryChange"
<van-tab title="学习工具" name="study" /> >
<van-tab title="生活工具" name="life" /> <CustomTabPane name="all" label="全部" />
</van-tabs> <CustomTabPane name="office" label="办公工具" />
<CustomTabPane name="creative" label="创意工具" />
<CustomTabPane name="study" label="学习工具" />
<CustomTabPane name="life" label="生活工具" />
</CustomTabs>
<!-- 工具列表 --> <!-- 工具列表 -->
<div class="tools-list"> <div class="tools-list">
@@ -75,9 +77,7 @@
<div class="recommend-content"> <div class="recommend-content">
<h4 class="recommend-title">{{ tool.name }}</h4> <h4 class="recommend-title">{{ tool.name }}</h4>
<p class="recommend-desc">{{ tool.desc }}</p> <p class="recommend-desc">{{ tool.desc }}</p>
<van-button size="small" type="primary" plain round> <van-button size="small" type="primary" plain round> 立即使用 </van-button>
立即使用
</van-button>
</div> </div>
</div> </div>
</van-swipe-item> </van-swipe-item>
@@ -86,7 +86,11 @@
</div> </div>
<!-- 工具详情弹窗 --> <!-- 工具详情弹窗 -->
<van-action-sheet v-model:show="showToolDetail" :title="selectedTool?.name"> <van-action-sheet
:model-value="showToolDetail"
@update:model-value="showToolDetail = $event"
:title="selectedTool && selectedTool.name"
>
<div class="tool-detail" v-if="selectedTool"> <div class="tool-detail" v-if="selectedTool">
<div class="detail-header"> <div class="detail-header">
<div class="detail-icon" :style="{ backgroundColor: selectedTool.color }"> <div class="detail-icon" :style="{ backgroundColor: selectedTool.color }">
@@ -129,7 +133,12 @@
</van-action-sheet> </van-action-sheet>
<!-- 思维导图工具 --> <!-- 思维导图工具 -->
<van-action-sheet v-model:show="showMindMap" title="思维导图" :close-on-click-overlay="false"> <van-action-sheet
:model-value="showMindMap"
@update:model-value="showMindMap = $event"
title="思维导图"
:close-on-click-overlay="false"
>
<div class="mindmap-container"> <div class="mindmap-container">
<div class="mindmap-toolbar"> <div class="mindmap-toolbar">
<van-button size="small" @click="createNewMap">新建</van-button> <van-button size="small" @click="createNewMap">新建</van-button>
@@ -151,6 +160,8 @@
</template> </template>
<script setup> <script setup>
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
import CustomTabs from '@/components/ui/CustomTabs.vue'
import { showNotify } from 'vant' import { showNotify } from 'vant'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -168,7 +179,8 @@ const tools = ref([
key: 'mindmap', key: 'mindmap',
name: '思维导图', name: '思维导图',
desc: '智能生成思维导图,整理思路更清晰', desc: '智能生成思维导图,整理思路更清晰',
fullDesc: '基于AI技术的智能思维导图生成工具可以根据文本内容自动生成结构化的思维导图支持多种导出格式。', fullDesc:
'基于AI技术的智能思维导图生成工具可以根据文本内容自动生成结构化的思维导图支持多种导出格式。',
icon: 'icon-mind', icon: 'icon-mind',
color: '#3B82F6', color: '#3B82F6',
category: 'office', category: 'office',
@@ -179,13 +191,14 @@ const tools = ref([
'提供多种精美模板', '提供多种精美模板',
'智能节点布局算法', '智能节点布局算法',
'支持导出多种格式', '支持导出多种格式',
'支持在线协作编辑' '支持在线协作编辑',
], ],
usage: '输入您的文本内容AI会自动分析并生成对应的思维导图结构。您可以对生成的导图进行编辑、美化和导出。', usage:
'输入您的文本内容AI会自动分析并生成对应的思维导图结构。您可以对生成的导图进行编辑、美化和导出。',
stats: { stats: {
usageCount: 1256, usageCount: 1256,
rating: 96 rating: 96,
} },
}, },
{ {
key: 'summary', key: 'summary',
@@ -202,13 +215,13 @@ const tools = ref([
'智能关键词提取', '智能关键词提取',
'可控制摘要长度', '可控制摘要长度',
'支持批量处理', '支持批量处理',
'多语言文档支持' '多语言文档支持',
], ],
usage: '上传或粘贴文档内容选择摘要长度和类型AI会自动生成文档摘要。', usage: '上传或粘贴文档内容选择摘要长度和类型AI会自动生成文档摘要。',
stats: { stats: {
usageCount: 2341, usageCount: 2341,
rating: 94 rating: 94,
} },
}, },
{ {
key: 'translation', key: 'translation',
@@ -225,13 +238,13 @@ const tools = ref([
'专业术语库支持', '专业术语库支持',
'上下文语境理解', '上下文语境理解',
'批量文档翻译', '批量文档翻译',
'翻译质量评估' '翻译质量评估',
], ],
usage: '选择源语言和目标语言输入需要翻译的内容AI会提供高质量的翻译结果。', usage: '选择源语言和目标语言输入需要翻译的内容AI会提供高质量的翻译结果。',
stats: { stats: {
usageCount: 5678, usageCount: 5678,
rating: 98 rating: 98,
} },
}, },
{ {
key: 'poster', key: 'poster',
@@ -248,13 +261,13 @@ const tools = ref([
'智能配色方案', '智能配色方案',
'自动排版布局', '自动排版布局',
'高清无水印导出', '高清无水印导出',
'支持自定义尺寸' '支持自定义尺寸',
], ],
usage: '选择海报类型和风格输入文案内容AI会自动生成专业海报设计。', usage: '选择海报类型和风格输入文案内容AI会自动生成专业海报设计。',
stats: { stats: {
usageCount: 3456, usageCount: 3456,
rating: 95 rating: 95,
} },
}, },
{ {
key: 'logo', key: 'logo',
@@ -271,13 +284,13 @@ const tools = ref([
'矢量格式输出', '矢量格式输出',
'商用版权授权', '商用版权授权',
'配色方案推荐', '配色方案推荐',
'标准化尺寸规范' '标准化尺寸规范',
], ],
usage: '描述您的品牌特点和期望风格AI会生成多个Logo设计方案供您选择。', usage: '描述您的品牌特点和期望风格AI会生成多个Logo设计方案供您选择。',
stats: { stats: {
usageCount: 2234, usageCount: 2234,
rating: 93 rating: 93,
} },
}, },
{ {
key: 'study-plan', key: 'study-plan',
@@ -294,13 +307,13 @@ const tools = ref([
'学习进度跟踪', '学习进度跟踪',
'智能计划调整', '智能计划调整',
'学习效果评估', '学习效果评估',
'多领域知识覆盖' '多领域知识覆盖',
], ],
usage: '输入您的学习目标、可用时间和当前水平AI会为您制定详细的学习计划。', usage: '输入您的学习目标、可用时间和当前水平AI会为您制定详细的学习计划。',
stats: { stats: {
usageCount: 1890, usageCount: 1890,
rating: 97 rating: 97,
} },
}, },
{ {
key: 'recipe', key: 'recipe',
@@ -317,13 +330,13 @@ const tools = ref([
'营养成分分析', '营养成分分析',
'详细制作步骤', '详细制作步骤',
'口味偏好适配', '口味偏好适配',
'热量控制建议' '热量控制建议',
], ],
usage: '拍照或输入现有食材AI会推荐适合的菜谱并提供详细制作指导。', usage: '拍照或输入现有食材AI会推荐适合的菜谱并提供详细制作指导。',
stats: { stats: {
usageCount: 567, usageCount: 567,
rating: 89 rating: 89,
} },
}, },
{ {
key: 'workout', key: 'workout',
@@ -340,19 +353,19 @@ const tools = ref([
'科学运动指导', '科学运动指导',
'训练进度跟踪', '训练进度跟踪',
'饮食建议搭配', '饮食建议搭配',
'健康数据分析' '健康数据分析',
], ],
usage: '输入您的身体状况、运动目标和时间安排AI会制定适合的运动计划。', usage: '输入您的身体状况、运动目标和时间安排AI会制定适合的运动计划。',
stats: { stats: {
usageCount: 234, usageCount: 234,
rating: 91 rating: 91,
} },
} },
]) ])
// 推荐工具取前3个可用的 // 推荐工具取前3个可用的
const recommendTools = computed(() => { const recommendTools = computed(() => {
return tools.value.filter(tool => tool.status === 'available').slice(0, 3) return tools.value.filter((tool) => tool.status === 'available').slice(0, 3)
}) })
// 根据分类筛选工具 // 根据分类筛选工具
@@ -360,7 +373,7 @@ const filteredTools = computed(() => {
if (activeCategory.value === 'all') { if (activeCategory.value === 'all') {
return tools.value return tools.value
} }
return tools.value.filter(tool => tool.category === activeCategory.value) return tools.value.filter((tool) => tool.category === activeCategory.value)
}) })
onMounted(() => { onMounted(() => {
@@ -368,7 +381,7 @@ onMounted(() => {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const toolKey = urlParams.get('tool') const toolKey = urlParams.get('tool')
if (toolKey) { if (toolKey) {
const tool = tools.value.find(t => t.key === toolKey) const tool = tools.value.find((t) => t.key === toolKey)
if (tool) { if (tool) {
openTool(tool) openTool(tool)
} }
@@ -558,7 +571,7 @@ const closeMindMap = () => {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
background: linear-gradient(135deg, var(--van-primary-color), #8B5CF6); background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
cursor: pointer; cursor: pointer;
.recommend-bg { .recommend-bg {