diff --git a/web/src/main.js b/web/src/main.js index 1510fd9f..2b8b9919 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -5,17 +5,21 @@ // * @Author yangjian102621@163.com // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -import { createApp } from "vue"; -import ElementPlus from "element-plus"; -import "element-plus/dist/index.css"; -import "@/assets/iconfont/iconfont.css"; -import "vant/lib/index.css"; -import App from "./App.vue"; -import { useThemeStore } from "@/store/theme"; -import { createPinia } from "pinia"; -import "animate.css/animate.min.css"; -import "@/assets/css/tailwind.css"; +import '@/assets/css/tailwind.css' +import '@/assets/iconfont/iconfont.css' +import { useThemeStore } from '@/store/theme' +import 'animate.css/animate.min.css' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import { createPinia } from 'pinia' +import 'vant/lib/index.css' +import { createApp } from 'vue' +import App from './App.vue' +import '@/assets/css/common.scss' +import '@/assets/css/theme-dark.scss' +import '@/assets/css/theme-light.scss' +import { router } from '@/router' import { ActionSheet, Badge, @@ -48,6 +52,8 @@ import { Overlay, Picker, Popup, + Radio, + RadioGroup, Row, Search, ShareSheet, @@ -61,64 +67,64 @@ import { Tabs, Tag, TextEllipsis, + Toast, Uploader, -} from "vant"; -import { router } from "@/router"; -import "@/assets/css/theme-dark.scss"; -import "@/assets/css/theme-light.scss"; -import "@/assets/css/common.scss"; +} from 'vant' -const pinia = createPinia(); -const themeStore = useThemeStore(pinia); // 使用 theme store +const pinia = createPinia() +const themeStore = useThemeStore(pinia) // 使用 theme store // 设置初始主题 -document.documentElement.setAttribute("data-theme", themeStore.theme); +document.documentElement.setAttribute('data-theme', themeStore.theme) -const app = createApp(App); -app.use(createPinia()); -app.use(ConfigProvider); -app.use(Tabbar); -app.use(TabbarItem); -app.use(NavBar); -app.use(Search); -app.use(Cell); -app.use(Image); -app.use(TextEllipsis); -app.use(Notify); -app.use(Picker); -app.use(Popup); -app.use(List); -app.use(Form); -app.use(Field); -app.use(CellGroup); -app.use(Button); -app.use(DropdownMenu); -app.use(Icon); -app.use(DropdownItem); -app.use(Sticky); -app.use(SwipeCell); -app.use(Dialog); -app.use(ShareSheet); -app.use(Switch); -app.use(Uploader); -app.use(Tag); -app.use(Overlay); -app.use(Col); -app.use(Row); -app.use(Slider); -app.use(Badge); -app.use(Collapse); -app.use(CollapseItem); -app.use(Grid); -app.use(GridItem); -app.use(Empty); -app.use(Circle); -app.use(Loading); -app.use(Lazyload); -app.use(ImagePreview); -app.use(Tab); -app.use(Tabs); -app.use(Divider); -app.use(NoticeBar); -app.use(ActionSheet); -app.use(router).use(ElementPlus).mount("#app"); +const app = createApp(App) +app.use(createPinia()) +app.use(ConfigProvider) +app.use(Tabbar) +app.use(TabbarItem) +app.use(NavBar) +app.use(Search) +app.use(Cell) +app.use(Image) +app.use(TextEllipsis) +app.use(Notify) +app.use(Picker) +app.use(Popup) +app.use(Radio) +app.use(RadioGroup) +app.use(List) +app.use(Form) +app.use(Field) +app.use(CellGroup) +app.use(Button) +app.use(DropdownMenu) +app.use(Icon) +app.use(DropdownItem) +app.use(Sticky) +app.use(SwipeCell) +app.use(Dialog) +app.use(ShareSheet) +app.use(Switch) +app.use(Uploader) +app.use(Tag) +app.use(Overlay) +app.use(Col) +app.use(Row) +app.use(Slider) +app.use(Badge) +app.use(Collapse) +app.use(CollapseItem) +app.use(Grid) +app.use(GridItem) +app.use(Empty) +app.use(Circle) +app.use(Loading) +app.use(Lazyload) +app.use(ImagePreview) +app.use(Tab) +app.use(Tabs) +app.use(Divider) +app.use(NoticeBar) +app.use(ActionSheet) +app.use(Toast) +app.use(router).use(ElementPlus).mount('#app') diff --git a/web/src/router.js b/web/src/router.js index a4396afd..99a1a3c5 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -309,15 +309,25 @@ const routes = [ component: () => import('@/views/mobile/ChatList.vue'), }, { - path: '/mobile/image', - name: 'mobile-image', - component: () => import('@/views/mobile/Image.vue'), + path: '/mobile/create', + name: 'mobile-create', + component: () => import('@/views/mobile/Create.vue'), + }, + { + path: '/mobile/discover', + name: 'mobile-discover', + component: () => import('@/views/mobile/Discover.vue'), }, { path: '/mobile/profile', name: 'mobile-profile', component: () => import('@/views/mobile/Profile.vue'), }, + { + path: '/mobile/member', + name: 'mobile-member', + component: () => import('@/views/mobile/Member.vue'), + }, { path: '/mobile/imgWall', name: 'mobile-img-wall', @@ -338,6 +348,37 @@ const routes = [ name: 'mobile-apps', component: () => import('@/views/mobile/Apps.vue'), }, + // 新增的功能页面路由 + { + path: '/mobile/power-log', + name: 'mobile-power-log', + component: () => import('@/views/mobile/PowerLog.vue'), + }, + { + path: '/mobile/invite', + name: 'mobile-invite', + component: () => import('@/views/mobile/Invite.vue'), + }, + { + path: '/mobile/tools', + name: 'mobile-tools', + component: () => import('@/views/mobile/Tools.vue'), + }, + { + path: '/mobile/settings', + name: 'mobile-settings', + component: () => import('@/views/mobile/Settings.vue'), + }, + { + path: '/mobile/help', + name: 'mobile-help', + component: () => import('@/views/mobile/Help.vue'), + }, + { + path: '/mobile/feedback', + name: 'mobile-feedback', + component: () => import('@/views/mobile/Feedback.vue'), + }, ], }, diff --git a/web/src/views/mobile/Create.vue b/web/src/views/mobile/Create.vue new file mode 100644 index 00000000..1fa18c5d --- /dev/null +++ b/web/src/views/mobile/Create.vue @@ -0,0 +1,169 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/mobile/Discover.vue b/web/src/views/mobile/Discover.vue new file mode 100644 index 00000000..6ca15b12 --- /dev/null +++ b/web/src/views/mobile/Discover.vue @@ -0,0 +1,328 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/mobile/Feedback.vue b/web/src/views/mobile/Feedback.vue new file mode 100644 index 00000000..8df848a7 --- /dev/null +++ b/web/src/views/mobile/Feedback.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/web/src/views/mobile/Help.vue b/web/src/views/mobile/Help.vue new file mode 100644 index 00000000..a151c43d --- /dev/null +++ b/web/src/views/mobile/Help.vue @@ -0,0 +1,846 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/mobile/Home.vue b/web/src/views/mobile/Home.vue index a9dd1390..50203f21 100644 --- a/web/src/views/mobile/Home.vue +++ b/web/src/views/mobile/Home.vue @@ -3,11 +3,37 @@
- - 首页 - 对话 - 绘图 - 我的 + + + 首页 + + + + 对话 + + + + 创作 + + + + 发现 + + + + 我的 + +
@@ -38,9 +64,32 @@ watch( position: fixed; width: 100%; } - padding: 0 10px; } + + .van-tabbar { + box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1); + + .van-tabbar-item { + .active-icon { + color: var(--van-primary-color) !important; + transform: scale(1.1); + transition: all 0.3s ease; + } + + &--active { + .van-tabbar-item__text { + color: var(--van-primary-color); + font-weight: 600; + } + } + } + + .iconfont { + font-size: 20px; + transition: all 0.3s ease; + } + } } // 黑色主题 diff --git a/web/src/views/mobile/Index.vue b/web/src/views/mobile/Index.vue index 91af1e5b..5747450c 100644 --- a/web/src/views/mobile/Index.vue +++ b/web/src/views/mobile/Index.vue @@ -1,86 +1,150 @@ @@ -99,20 +163,53 @@ const isLogin = ref(false) const apps = ref([]) const loading = ref(false) const roles = ref([]) -const slogan = ref('你有多大想象力,AI就有多大创造力!') - -// 只显示前5个应用 -const displayApps = computed(() => { - return apps.value.slice(0, 8) +const userAvatar = ref('/images/avatar/default.jpg') +const userStats = ref({ + conversations: 0, + images: 0, + power: 0 }) +// 功能配置 +const features = ref([ + { key: 'mj', name: 'MJ绘画', icon: 'icon-mj', color: '#8B5CF6', url: '/mobile/create?tab=mj' }, + { key: 'sd', name: 'SD绘画', icon: 'icon-sd', color: '#06B6D4', url: '/mobile/create?tab=sd' }, + { key: 'dalle', name: 'DALL·E', icon: 'icon-dalle', color: '#F59E0B', url: '/mobile/create?tab=dalle' }, + { key: 'suno', name: '音乐创作', icon: 'icon-music', color: '#EF4444', url: '/mobile/create?tab=suno' }, + { key: 'video', name: '视频生成', icon: 'icon-video', color: '#10B981', url: '/mobile/create?tab=video' }, + { key: 'jimeng', name: '即梦AI', icon: 'icon-jimeng', color: '#F97316', url: '/mobile/create?tab=jimeng' }, + { key: 'xmind', name: '思维导图', icon: 'icon-mind', color: '#3B82F6', url: '/mobile/tools?tab=xmind' }, + { key: 'imgWall', name: '作品展示', icon: 'icon-gallery', color: '#EC4899', url: '/mobile/imgWall' } +]) + +// 应用分组显示(每行2个) +const appChunks = computed(() => { + const chunks = [] + const displayApps = apps.value.slice(0, 6) // 只显示前6个 + for (let i = 0; i < displayApps.length; i += 2) { + chunks.push(displayApps.slice(i, i + 2)) + } + return chunks +}) + +// 获取问候语 +const getGreeting = () => { + const hour = new Date().getHours() + if (hour < 6) return '夜深了' + if (hour < 12) return '早上好' + if (hour < 18) return '下午好' + return '晚上好' +} + +// 导航到功能页面 +const navigateToFeature = (feature) => { + router.push(feature.url) +} + onMounted(() => { getSystemInfo() .then((res) => { title.value = res.data.title - if (res.data.slogan) { - slogan.value = res.data.slogan - } }) .catch((e) => { ElMessage.error('获取系统配置失败:' + e.message) @@ -122,8 +219,12 @@ onMounted(() => { .then((user) => { isLogin.value = true roles.value = user.chat_roles + userAvatar.value = user.avatar || '/images/avatar/default.jpg' + // 获取用户统计数据 + fetchUserStats() }) .catch(() => {}) + fetchApps() }) @@ -133,7 +234,7 @@ const fetchApps = () => { const items = res.data // 处理 hello message for (let i = 0; i < items.length; i++) { - items[i].intro = substr(items[i].hello_msg, 80) + items[i].intro = substr(items[i].hello_msg, 30) } apps.value = items }) @@ -142,6 +243,22 @@ const fetchApps = () => { }) } +const fetchUserStats = () => { + if (!isLogin.value) return + + // 这里可以调用实际的统计接口 + // httpGet('/api/user/stats').then(res => { + // userStats.value = res.data + // }) + + // 临时使用模拟数据 + userStats.value = { + conversations: Math.floor(Math.random() * 100), + images: Math.floor(Math.random() * 50), + power: Math.floor(Math.random() * 1000) + } +} + const updateRole = (row, opt) => { if (!isLogin.value) { return showLoginDialog(router) @@ -165,10 +282,10 @@ const updateRole = (row, opt) => { } httpPost('/api/app/update', { keys: roles.value }) .then(() => { - ElMessage.success({ message: title.value + '成功!', duration: 1000 }) + showNotify({ type: 'success', message: title.value + '成功!', duration: 1000 }) }) .catch((e) => { - ElMessage.error(title.value + '失败:' + e.message) + showNotify({ type: 'danger', message: title.value + '失败:' + e.message }) }) } @@ -187,148 +304,299 @@ const useRole = (roleId) => { diff --git a/web/src/views/mobile/Invite.vue b/web/src/views/mobile/Invite.vue new file mode 100644 index 00000000..11e70d21 --- /dev/null +++ b/web/src/views/mobile/Invite.vue @@ -0,0 +1,739 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/mobile/Member.vue b/web/src/views/mobile/Member.vue new file mode 100644 index 00000000..670e2773 --- /dev/null +++ b/web/src/views/mobile/Member.vue @@ -0,0 +1,608 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/mobile/PowerLog.vue b/web/src/views/mobile/PowerLog.vue new file mode 100644 index 00000000..6266d89a --- /dev/null +++ b/web/src/views/mobile/PowerLog.vue @@ -0,0 +1,479 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/mobile/Profile.vue b/web/src/views/mobile/Profile.vue index 46545766..e777c0ef 100644 --- a/web/src/views/mobile/Profile.vue +++ b/web/src/views/mobile/Profile.vue @@ -1,142 +1,314 @@ @@ -148,244 +320,632 @@ import { httpGet, httpPost } from '@/utils/http' import { dateFormat, showLoginDialog } from '@/utils/libs' import { ElMessage } from 'element-plus' import { showFailToast, showLoadingToast, showNotify, showSuccessToast } from 'vant' -import { onMounted, ref } from 'vue' +import { computed, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' const form = ref({ + id: 0, username: 'GeekMaster', nickname: '极客学长@001', mobile: '1300000000', avatar: '', power: 0, + expired_time: 0, }) + const fileList = ref([ { - url: '/images/user-info.png', + url: '/images/avatar/default.jpg', message: '上传中...', }, ]) -const products = ref([]) -const vipMonthPower = ref(0) -const payWays = ref({}) const router = useRouter() -const userId = ref(0) const isLogin = ref(false) const showSettings = ref(false) +const showPasswordDialog = ref(false) +const showAvatarOptions = ref(false) +const showAbout = ref(false) +const showLogoutConfirm = ref(false) const store = useSharedStore() const stream = ref(store.chatStream) const dark = ref(store.theme === 'dark') -const menuList = ref({}) +const title = ref(import.meta.env.VITE_TITLE) +const appVersion = ref('2.1.0') -onMounted(() => { - checkSession() - .then((user) => { - userId.value = user.id - isLogin.value = true - httpGet('/api/user/profile') - .then((res) => { - form.value = res.data - fileList.value[0].url = form.value.avatar - }) - .catch((e) => { - console.log(e.message) - showFailToast('获取用户信息失败') - }) - }) - .catch(() => {}) +// 新增状态 +const notifications = ref(true) +const autoSave = ref(true) +const inviteCount = ref(0) +const passwordForm = ref() - // 获取产品列表 - httpGet('/api/product/list') - .then((res) => { - products.value = res.data - }) - .catch((e) => { - showFailToast('获取产品套餐失败:' + e.message) - }) - - getSystemInfo() - .then((res) => { - vipMonthPower.value = res.data['vip_month_power'] - }) - .catch((e) => { - showFailToast('获取系统配置失败:' + e.message) - }) - - httpGet('/api/payment/payWays') - .then((res) => { - payWays.value = res.data - }) - .catch((e) => { - ElMessage.error('获取支付方式失败:' + e.message) - }) - - getMenuList() -}) - -// 获取菜单列表 -const getMenuList = () => { - httpGet('/api/menu/list') - .then((res) => { - res.data.forEach((item) => { - menuList.value[item.url] = item - }) - }) - .catch((e) => { - showFailToast('获取菜单列表失败:' + e.message) - }) -} - -const showPasswordDialog = ref(false) +// 密码相关 const pass = ref({ old: '', new: '', renew: '', }) -const beforeClose = (action) => { - new Promise((resolve) => { - resolve(action === 'confirm') - }) +// 密码验证规则 +const passwordRules = [ + { required: true, message: '请输入新密码' }, + { min: 8, max: 16, message: '密码长度为8-16个字符' } +] + +// 计算属性 +const isVip = computed(() => { + const now = Date.now() + const expiredTime = form.value.expired_time ? form.value.expired_time * 1000 : 0 + return expiredTime > now +}) + +const vipDays = computed(() => { + if (!isVip.value) return 0 + const now = Date.now() + const expiredTime = form.value.expired_time * 1000 + return Math.ceil((expiredTime - now) / (24 * 60 * 60 * 1000)) +}) + +onMounted(() => { + getSystemInfo() + .then((res) => { + title.value = res.data.title + }) + .catch((e) => { + console.error('获取系统配置失败:', e.message) + }) + + checkSession() + .then((user) => { + isLogin.value = true + form.value = { ...form.value, ...user } + fileList.value[0].url = user.avatar || '/images/avatar/default.jpg' + + // 获取用户详细信息 + fetchUserProfile() + fetchUserStats() + }) + .catch(() => { + isLogin.value = false + }) +}) + +// 获取用户详细信息 +const fetchUserProfile = () => { + httpGet('/api/user/profile') + .then((res) => { + form.value = { ...form.value, ...res.data } + fileList.value[0].url = res.data.avatar || '/images/avatar/default.jpg' + }) + .catch((e) => { + console.error('获取用户信息失败:', e.message) + }) +} + +// 获取用户统计信息 +const fetchUserStats = () => { + // 模拟数据,实际项目中应调用API + inviteCount.value = Math.floor(Math.random() * 20) +} + +// 确认密码验证 +const validateConfirmPassword = (value) => { + if (value !== pass.value.new) { + return Promise.reject(new Error('两次输入的密码不一致')) + } + return Promise.resolve() +} + +// 重置密码表单 +const resetPasswordForm = () => { + pass.value = { + old: '', + new: '', + renew: '', + } + if (passwordForm.value) { + passwordForm.value.resetValidation() + } } // 提交修改密码 const updatePass = () => { - if (pass.value.old === '') { + if (!passwordForm.value) { + updatePasswordAPI() + return + } + + passwordForm.value.validate().then(() => { + updatePasswordAPI() + }).catch((errors) => { + console.log('表单验证失败:', errors) + }) +} + +const updatePasswordAPI = () => { + if (!pass.value.old) { return showNotify({ type: 'danger', message: '请输入旧密码' }) } if (!pass.value.new || pass.value.new.length < 8) { - return showNotify({ type: 'danger', message: '密码的长度为8-16个字符' }) + return showNotify({ type: 'danger', message: '密码长度为8-16个字符' }) } if (pass.value.renew !== pass.value.new) { return showNotify({ type: 'danger', message: '两次输入密码不一致' }) } + + showLoadingToast({ + message: '正在修改密码...', + forbidClick: true, + }) + httpPost('/api/user/password', { old_pass: pass.value.old, password: pass.value.new, repass: pass.value.renew, }) .then(() => { - showSuccessToast('更新成功!') + showSuccessToast('密码修改成功!') showPasswordDialog.value = false + resetPasswordForm() }) .catch((e) => { - showFailToast('更新失败,' + e.message) - showPasswordDialog.value = false + showFailToast('密码修改失败:' + e.message) }) } -const pay = (product, payWay) => { - if (!isLogin.value) { - return showLoginDialog(router) +// 头像选择 +const selectAvatar = (type) => { + showAvatarOptions.value = false + + switch (type) { + case 'camera': + // 调用相机 + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + showNotify({ type: 'primary', message: '正在启动相机...' }) + } else { + showNotify({ type: 'warning', message: '您的设备不支持相机功能' }) + } + break + case 'album': + // 从相册选择 + const input = document.createElement('input') + input.type = 'file' + input.accept = 'image/*' + input.onchange = (e) => { + const file = e.target.files[0] + if (file) { + // 这里应该上传到服务器 + const reader = new FileReader() + reader.onload = (e) => { + fileList.value[0].url = e.target.result + showSuccessToast('头像更新成功') + } + reader.readAsDataURL(file) + } + } + input.click() + break + case 'default': + // 使用默认头像 + fileList.value[0].url = '/images/avatar/default.jpg' + showSuccessToast('已设置为默认头像') + break } +} +// 退出登录 +const logout = function () { showLoadingToast({ - message: '正在创建订单', + message: '正在退出...', forbidClick: true, }) - let host = process.env.VUE_APP_API_HOST - if (host === '') { - host = `${location.protocol}//${location.host}` - } - httpPost(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, { - product_id: product.id, - pay_way: payWay.pay_way, - pay_type: payWay.pay_type, - user_id: userId.value, - host: host, - device: 'wechat', - }) - .then((res) => { - location.href = res.data - }) - .catch((e) => { - showFailToast('生成支付订单失败:' + e.message) - }) -} -const logout = function () { httpGet('/api/user/logout') .then(() => { removeUserToken() store.setIsLogin(false) - router.push('/') + isLogin.value = false + showSuccessToast('退出登录成功') + showLogoutConfirm.value = false + + // 清除用户数据 + form.value = { + id: 0, + username: '', + nickname: '', + mobile: '', + avatar: '', + power: 0, + expired_time: 0, + } + fileList.value[0].url = '/images/avatar/default.jpg' }) - .catch(() => { - showFailToast('注销失败!') + .catch((e) => { + showFailToast('退出登录失败:' + e.message) }) } - diff --git a/web/src/views/mobile/Settings.vue b/web/src/views/mobile/Settings.vue new file mode 100644 index 00000000..42a040cf --- /dev/null +++ b/web/src/views/mobile/Settings.vue @@ -0,0 +1,631 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/mobile/Tools.vue b/web/src/views/mobile/Tools.vue new file mode 100644 index 00000000..f01ee6c9 --- /dev/null +++ b/web/src/views/mobile/Tools.vue @@ -0,0 +1,743 @@ + + + + + \ No newline at end of file