调整移动端App列表样式

This commit is contained in:
GeekMaster 2025-04-11 17:10:21 +08:00
parent 79522d9ab5
commit 4641482865
3 changed files with 517 additions and 153 deletions

View File

@ -321,6 +321,11 @@ const routes = [
name: 'mobile-chat-export', name: 'mobile-chat-export',
component: () => import('@/views/mobile/ChatExport.vue'), component: () => import('@/views/mobile/ChatExport.vue'),
}, },
{
path: '/mobile/apps',
name: 'mobile-apps',
component: () => import('@/views/mobile/Apps.vue'),
},
], ],
}, },

View File

@ -0,0 +1,268 @@
<template>
<div class="apps-page">
<van-nav-bar title="全部应用" left-arrow @click-left="router.back()" />
<div class="apps-filter mb-8 pt-8" style="border: 1px solid #ccc">
<van-tabs v-model="activeTab" animated swipeable>
<van-tab title="全部分类">
<div class="app-list">
<van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps()">
<van-cell v-for="item in apps" :key="item.id" class="app-cell">
<div class="app-card">
<div class="app-info">
<div class="app-image">
<van-image :src="item.icon" round />
</div>
<div class="app-detail">
<div class="app-title">{{ item.name }}</div>
<div class="app-desc">{{ item.hello_msg }}</div>
</div>
</div>
<div class="app-actions">
<van-button
size="small"
type="primary"
class="action-btn"
@click="useRole(item.id)"
>对话</van-button
>
<van-button
size="small"
:type="hasRole(item.key) ? 'danger' : 'success'"
class="action-btn"
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '移除' : '添加' }}
</van-button>
</div>
</div>
</van-cell>
</van-list>
</div>
</van-tab>
<van-tab v-for="type in appTypes" :key="type.id" :title="type.name">
<div class="app-list">
<van-list
v-model="loading"
:finished="true"
finished-text=""
@load="fetchApps(type.id)"
>
<van-cell v-for="item in typeApps" :key="item.id" class="app-cell">
<div class="app-card">
<div class="app-info">
<div class="app-image">
<van-image :src="item.icon" round />
</div>
<div class="app-detail">
<div class="app-title">{{ item.name }}</div>
<div class="app-desc">{{ item.hello_msg }}</div>
</div>
</div>
<div class="app-actions">
<van-button
size="small"
type="primary"
class="action-btn"
@click="useRole(item.id)"
>对话</van-button
>
<van-button
size="small"
:type="hasRole(item.key) ? 'danger' : 'success'"
class="action-btn"
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '移除' : '添加' }}
</van-button>
</div>
</div>
</van-cell>
</van-list>
</div>
</van-tab>
</van-tabs>
</div>
</div>
</template>
<script setup>
import { checkSession } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http'
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
import { showNotify } from 'vant'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLogin = ref(false)
const apps = ref([])
const typeApps = ref([])
const appTypes = ref([])
const loading = ref(false)
const roles = ref([])
const activeTab = ref(0)
onMounted(() => {
checkSession()
.then((user) => {
isLogin.value = true
roles.value = user.chat_roles
})
.catch(() => {})
fetchAppTypes()
fetchApps()
})
const fetchAppTypes = () => {
httpGet('/api/app/type/list')
.then((res) => {
appTypes.value = res.data
})
.catch((e) => {
showNotify({ type: 'danger', message: '获取应用分类失败:' + e.message })
})
}
const fetchApps = (typeId = '') => {
httpGet('/api/app/list', { tid: typeId })
.then((res) => {
const items = res.data
// hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80)
}
if (typeId) {
typeApps.value = items
} else {
apps.value = items
}
})
.catch((e) => {
showNotify({ type: 'danger', message: '获取应用失败:' + e.message })
})
}
const updateRole = (row, opt) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
const title = ref('')
if (opt === 'add') {
title.value = '添加应用'
const exists = arrayContains(roles.value, row.key)
if (exists) {
return
}
roles.value.push(row.key)
} else {
title.value = '移除应用'
const exists = arrayContains(roles.value, row.key)
if (!exists) {
return
}
roles.value = removeArrayItem(roles.value, row.key)
}
httpPost('/api/app/update', { keys: roles.value })
.then(() => {
showNotify({ type: 'success', message: title.value + '成功!' })
})
.catch((e) => {
showNotify({ type: 'danger', message: title.value + '失败:' + e.message })
})
}
const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
}
const useRole = (roleId) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
router.push(`/mobile/chat/session?role_id=${roleId}`)
}
</script>
<style scoped lang="stylus">
.apps-page {
min-height 100vh
background-color var(--van-background)
.apps-filter {
padding 10px 0
:deep(.van-tabs__nav) {
background var(--van-background-2)
}
}
.app-list {
padding 0 15px
.app-cell {
padding 0
margin-bottom 15px
.app-card {
background var(--van-cell-background)
border-radius 12px
padding 15px
box-shadow 0 2px 12px rgba(0, 0, 0, 0.05)
.app-info {
display flex
align-items center
margin-bottom 15px
.app-image {
width 60px
height 60px
margin-right 15px
:deep(.van-image) {
width 100%
height 100%
}
}
.app-detail {
flex 1
.app-title {
font-size 16px
font-weight 600
margin-bottom 5px
color var(--van-text-color)
}
.app-desc {
font-size 13px
color var(--van-gray-6)
display -webkit-box
-webkit-box-orient vertical
-webkit-line-clamp 2
overflow hidden
}
}
}
.app-actions {
display flex
gap 10px
.action-btn {
flex 1
border-radius 20px
padding 0 10px
}
}
}
}
}
}
</style>

View File

@ -1,60 +1,79 @@
<template> <template>
<div class="index container"> <div class="index container">
<h2 class="title">{{ title }}</h2> <div class="header">
<van-notice-bar left-icon="info-o" :scrollable="true">{{ slogan }}}</van-notice-bar> <h2 class="title">{{ title }}</h2>
</div>
<div class="content"> <div class="content mb-8">
<van-grid :column-num="3" :gutter="10" border> <div class="feature-grid">
<van-grid-item @click="router.push('chat')"> <van-grid :column-num="3" :gutter="15" border>
<template #icon> <van-grid-item @click="router.push('chat')" class="feature-item">
<i class="iconfont icon-chat"></i> <template #icon>
</template> <div class="feature-icon">
<template #text> <i class="iconfont icon-chat"></i>
<div class="text">AI 对话</div> </div>
</template> </template>
</van-grid-item> <template #text>
<div class="text">AI 对话</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('image')"> <van-grid-item @click="router.push('image')" class="feature-item">
<template #icon> <template #icon>
<i class="iconfont icon-mj"></i> <div class="feature-icon">
</template> <i class="iconfont icon-mj"></i>
<template #text> </div>
<div class="text">AI 绘画</div> </template>
</template> <template #text>
</van-grid-item> <div class="text">AI 绘画</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('imgWall')"> <van-grid-item @click="router.push('imgWall')" class="feature-item">
<template #icon> <template #icon>
<van-icon name="photo-o" /> <div class="feature-icon">
</template> <van-icon name="photo-o" />
<template #text> </div>
<div class="text">AI 画廊</div> </template>
</template> <template #text>
</van-grid-item> <div class="text">AI 画廊</div>
</van-grid> </template>
</van-grid-item>
</van-grid>
</div>
<div class="section-header">
<h3 class="section-title">推荐应用</h3>
<van-button class="more-btn" size="small" icon="arrow" @click="router.push('apps')"
>更多</van-button
>
</div>
<div class="app-list"> <div class="app-list">
<van-list v-model:loading="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"> <van-cell v-for="item in displayApps" :key="item.id" class="app-cell">
<div> <div class="app-card">
<div class="item"> <div class="app-info">
<div class="image flex justify-center items-center"> <div class="app-image">
<van-image :src="item.icon" /> <van-image :src="item.icon" round />
</div> </div>
<div class="info"> <div class="app-detail">
<div class="info-title">{{ item.name }}</div> <div class="app-title">{{ item.name }}</div>
<div class="info-text">{{ item.hello_msg }}</div> <div class="app-desc">{{ item.hello_msg }}</div>
</div> </div>
</div> </div>
<div class="btn"> <div class="app-actions">
<div v-if="hasRole(item.key)"> <van-button size="small" type="primary" class="action-btn" @click="useRole(item.id)"
<van-button size="small" type="success" @click="useRole(item.id)">使用</van-button> >对话</van-button
<van-button size="small" type="danger" @click="updateRole(item, 'remove')">移除</van-button> >
</div> <van-button
<van-button v-else size="small" style="--el-color-primary: #009999" @click="updateRole(item, 'add')"> size="small"
<van-icon name="add-o" /> :type="hasRole(item.key) ? 'danger' : 'success'"
<span>添加应用</span> class="action-btn"
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '移除' : '添加' }}
</van-button> </van-button>
</div> </div>
</div> </div>
@ -66,173 +85,245 @@
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { checkSession, getSystemInfo } from '@/store/cache'
import { useRouter } from "vue-router"; import { httpGet, httpPost } from '@/utils/http'
import { checkSession, getSystemInfo } from "@/store/cache"; import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
import { httpGet, httpPost } from "@/utils/http"; import { ElMessage } from 'element-plus'
import { arrayContains, removeArrayItem, showLoginDialog, substr } from "@/utils/libs"; import { showNotify } from 'vant'
import { showNotify } from "vant"; import { computed, onMounted, ref } from 'vue'
import { ElMessage } from "element-plus"; import { useRouter } from 'vue-router'
const title = ref(process.env.VUE_APP_TITLE); const title = ref(process.env.VUE_APP_TITLE)
const router = useRouter(); const router = useRouter()
const isLogin = ref(false); const isLogin = ref(false)
const apps = ref([]); const apps = ref([])
const loading = ref(false); const loading = ref(false)
const roles = ref([]); const roles = ref([])
const slogan = ref("你有多大想象力AI就有多大创造力"); const slogan = ref('你有多大想象力AI就有多大创造力')
// 5
const displayApps = computed(() => {
return apps.value.slice(0, 8)
})
onMounted(() => { onMounted(() => {
getSystemInfo() getSystemInfo()
.then((res) => { .then((res) => {
title.value = res.data.title; title.value = res.data.title
if (res.data.slogan) { if (res.data.slogan) {
slogan.value = res.data.slogan; slogan.value = res.data.slogan
} }
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message); ElMessage.error('获取系统配置失败:' + e.message)
}); })
checkSession() checkSession()
.then((user) => { .then((user) => {
isLogin.value = true; isLogin.value = true
roles.value = user.chat_roles; roles.value = user.chat_roles
}) })
.catch(() => {}); .catch(() => {})
fetchApps(); fetchApps()
}); })
const fetchApps = () => { const fetchApps = () => {
httpGet("/api/app/list") httpGet('/api/app/list')
.then((res) => { .then((res) => {
const items = res.data; const items = res.data
// hello message // hello message
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80); items[i].intro = substr(items[i].hello_msg, 80)
} }
apps.value = items; apps.value = items
}) })
.catch((e) => { .catch((e) => {
showNotify({ type: "danger", message: "获取应用失败:" + e.message }); showNotify({ type: 'danger', message: '获取应用失败:' + e.message })
}); })
}; }
const updateRole = (row, opt) => { const updateRole = (row, opt) => {
if (!isLogin.value) { if (!isLogin.value) {
return showLoginDialog(router); return showLoginDialog(router)
} }
const title = ref(""); const title = ref('')
if (opt === "add") { if (opt === 'add') {
title.value = "添加应用"; title.value = '添加应用'
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 = "移除应用"; title.value = '移除应用'
const exists = arrayContains(roles.value, row.key); const exists = arrayContains(roles.value, row.key)
if (!exists) { if (!exists) {
return; return
} }
roles.value = removeArrayItem(roles.value, row.key); roles.value = removeArrayItem(roles.value, row.key)
} }
httpPost("/api/role/update", { keys: roles.value }) httpPost('/api/app/update', { keys: roles.value })
.then(() => { .then(() => {
ElMessage.success({ message: title.value + "成功!", duration: 1000 }); ElMessage.success({ message: title.value + '成功!', duration: 1000 })
}) })
.catch((e) => { .catch((e) => {
ElMessage.error(title.value + "失败:" + e.message); ElMessage.error(title.value + '失败:' + e.message)
}); })
}; }
const hasRole = (roleKey) => { const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2); return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
}; }
const useRole = (roleId) => { const useRole = (roleId) => {
if (!isLogin.value) { if (!isLogin.value) {
return showLoginDialog(router); return showLoginDialog(router)
} }
router.push(`/mobile/chat/session?role_id=${roleId}`); router.push(`/mobile/chat/session?role_id=${roleId}`)
}; }
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
.index { .index {
color var(--van-text-color) color var(--van-text-color)
.title { background-color var(--van-background)
display flex min-height 100vh
justify-content center display flex
flex-direction column
.header {
flex-shrink 0
padding 10px 15px
text-align center
background var(--van-background)
position sticky
top 0
z-index 1
.title {
font-size 24px
font-weight 600
color var(--van-text-color)
}
.slogan {
font-size 14px
color var(--van-gray-6)
}
} }
--van-notice-bar-font-size: 16px
.content { .content {
padding 15px 0 60px 0 flex 1
.van-grid-item { overflow-y auto
.iconfont { padding 15px
font-size 20px -webkit-overflow-scrolling touch
.feature-grid {
margin-bottom 30px
.feature-item {
padding 15px 0
.feature-icon {
width 50px
height 50px
border-radius 50%
background var(--van-primary-color)
display flex
align-items center
justify-content center
margin-bottom 10px
i, .van-icon {
font-size 24px
color white
}
}
.text {
font-size 14px
font-weight 500
}
} }
.text { }
display flex
width 100% .section-header {
padding 10px display flex
justify-content center justify-content space-between
font-size 14px align-items center
margin-bottom 15px
.section-title {
font-size 18px
font-weight 600
color var(--van-text-color)
}
.more-btn {
padding 0 10px
font-size 12px
border-radius 15px
} }
} }
.app-list { .app-list {
padding-top 10px .app-cell {
padding 0
margin-bottom 15px
.item { .app-card {
display flex background var(--van-cell-background)
.image { border-radius 12px
width 80px padding 15px
height 80px box-shadow 0 2px 12px rgba(0, 0, 0, 0.05)
min-width 80px
border-radius 5px
overflow hidden
}
.info { .app-info {
text-align left display flex
padding 0 10px align-items center
.info-title { margin-bottom 15px
color var(--van-text-color)
font-size 1.25rem .app-image {
line-height 1.75rem width 60px
letter-spacing: .025em; height 60px
font-weight: 600; margin-right 15px
word-break: break-all;
overflow: hidden; :deep(.van-image) {
display: -webkit-box; width 100%
-webkit-box-orient: vertical; height 100%
-webkit-line-clamp: 1; }
}
.app-detail {
flex 1
.app-title {
font-size 16px
font-weight 600
margin-bottom 5px
color var(--van-text-color)
}
.app-desc {
font-size 13px
color var(--van-gray-6)
display -webkit-box
-webkit-box-orient vertical
-webkit-line-clamp 2
overflow hidden
}
}
} }
.info-text { .app-actions {
padding 5px 0 display flex
overflow: hidden; gap 10px
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-all;
font-size: .875rem;
}
}
}
.btn { .action-btn {
padding 5px 0 flex 1
.van-button { border-radius 20px
margin-right 10px padding 0 10px
}
.van-icon {
margin-right 5px
} }
} }
} }