feat: change theme for mobile site is ready

This commit is contained in:
RockYang 2024-04-30 18:57:15 +08:00
parent 51a8f42d89
commit 1ba08bbfa6
17 changed files with 203 additions and 81 deletions

View File

@ -7,8 +7,12 @@
* 功能优化:优化首页登录注册页面的 UI * 功能优化:优化首页登录注册页面的 UI
* BUG修复修复License验证的逻辑漏洞 * BUG修复修复License验证的逻辑漏洞
* Bug修复后台添加用户的时候密码规则限制跟前台注册保持一致 * Bug修复后台添加用户的时候密码规则限制跟前台注册保持一致
* 功能新增:管理后台支持切换主题,支持 light 和 dark 模式 * 功能新增:管理后台支持切换主题,支持 light 和 dark 两种主题
* 功能新增:移动端新增 DALL-E 绘画功能 * 功能新增:移动端新增 DALL-E 绘画功能
* 功能新增:新增移动端首页功能,移动端支持 light 和 dark 两种主题
* 功能新增:移动支持免登录预览功能
* Bug修复解决在同一个浏览器开启多个对话时候对话内容会相互乱串的问题
* Bug修复修复部分中转 API 模型会出现第一输出的字符被淹没的Bug
## v4.0.4 ## v4.0.4

View File

@ -5,7 +5,6 @@ const LoginUserCache = "LOGIN_USER_CACHE"
const UserAuthHeader = "Authorization" const UserAuthHeader = "Authorization"
const AdminAuthHeader = "Admin-Authorization" const AdminAuthHeader = "Admin-Authorization"
const ChatTokenHeader = "Chat-Token"
// Session configs struct // Session configs struct
type Session struct { type Session struct {

View File

@ -139,6 +139,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
if err != nil { if err != nil {
client.Close() client.Close()
h.App.ChatClients.Delete(sessionId) h.App.ChatClients.Delete(sessionId)
h.App.ChatSession.Delete(sessionId)
cancelFunc := h.App.ReqCancelFunc.Get(sessionId) cancelFunc := h.App.ReqCancelFunc.Get(sessionId)
if cancelFunc != nil { if cancelFunc != nil {
cancelFunc() cancelFunc()

View File

@ -115,11 +115,8 @@ func (h *ChatHandler) sendOpenAiMessage(
break break
} }
// 初始化 role // output stopped
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" { if responseBody.Choices[0].FinishReason != "" {
message.Role = responseBody.Choices[0].Delta.Role
continue
} else if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了 break // 输出完成或者输出中断了
} else { } else {
content := responseBody.Choices[0].Delta.Content content := responseBody.Choices[0].Delta.Content

View File

@ -232,18 +232,10 @@ func (h *UserHandler) Login(c *gin.Context) {
// Logout 注 销 // Logout 注 销
func (h *UserHandler) Logout(c *gin.Context) { func (h *UserHandler) Logout(c *gin.Context) {
sessionId := c.GetHeader(types.ChatTokenHeader)
key := h.GetUserKey(c) key := h.GetUserKey(c)
if _, err := h.redis.Del(c, key).Result(); err != nil { if _, err := h.redis.Del(c, key).Result(); err != nil {
logger.Error("error with delete session: ", err) logger.Error("error with delete session: ", err)
} }
// 删除 websocket 会话列表
h.App.ChatSession.Delete(sessionId)
// 关闭 socket 连接
client := h.App.ChatClients.Get(sessionId)
if client != nil {
client.Close()
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }

View File

@ -26,6 +26,7 @@
"markmap-lib": "^0.16.1", "markmap-lib": "^0.16.1",
"markmap-view": "^0.16.0", "markmap-view": "^0.16.0",
"md-editor-v3": "^2.2.1", "md-editor-v3": "^2.2.1",
"mitt": "^3.0.1",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.1", "qs": "^6.11.1",

View File

@ -8,7 +8,7 @@
.rate { .rate {
display: flex; display: flex;
justify-content center justify-content center
background-color #f5f5f5 background-color var(--van-background-3)
padding 5px 10px padding 5px 10px
margin 5px 0 margin 5px 0
border-radius 5px border-radius 5px
@ -24,14 +24,14 @@
.text { .text {
text-align center text-align center
color #555555 color var(--van-text-color)
} }
} }
.model { .model {
display: flex; display: flex;
justify-content center justify-content center
background-color #f5f5f5 background-color var(--van-background-3)
padding 6px padding 6px
margin 5px 0 margin 5px 0
border-radius 5px border-radius 5px
@ -48,12 +48,12 @@
.text { .text {
text-align center text-align center
color #555555 color var(--van-text-color)
} }
} }
.active { .active {
background-color #e5e5e5 background-color var(--van-text-color-3)
} }
} }
} }

View File

@ -5,6 +5,7 @@ import 'vant/lib/index.css';
import App from './App.vue' import App from './App.vue'
import {createPinia} from "pinia"; import {createPinia} from "pinia";
import { import {
ActionSheet,
Badge, Badge,
Button, Button,
Cell, Cell,
@ -14,7 +15,8 @@ import {
Collapse, Collapse,
CollapseItem, CollapseItem,
ConfigProvider, ConfigProvider,
Dialog, Divider, Dialog,
Divider,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
Empty, Empty,
@ -28,7 +30,8 @@ import {
Lazyload, Lazyload,
List, List,
Loading, Loading,
NavBar, NoticeBar, NavBar,
NoticeBar,
Notify, Notify,
Overlay, Overlay,
Picker, Picker,
@ -52,8 +55,8 @@ import {router} from "@/router";
import 'v3-waterfall/dist/style.css' import 'v3-waterfall/dist/style.css'
import V3waterfall from "v3-waterfall"; import V3waterfall from "v3-waterfall";
const app = createApp(App) const app = createApp(App);
app.use(createPinia()) app.use(createPinia());
app.use(ConfigProvider); app.use(ConfigProvider);
app.use(Tabbar); app.use(Tabbar);
app.use(TabbarItem); app.use(TabbarItem);
@ -99,6 +102,7 @@ app.use(Tab);
app.use(Tabs); app.use(Tabs);
app.use(Divider); app.use(Divider);
app.use(NoticeBar); app.use(NoticeBar);
app.use(ActionSheet);
app.use(router).use(ElementPlus).mount('#app') app.use(router).use(ElementPlus).mount('#app')

View File

@ -0,0 +1,6 @@
// 导入mitt包
import mitt from 'mitt'
// 创建EventBus实例对象
const bus = mitt()
// 共享出eventbus的实例对象
export default bus

View File

@ -5,25 +5,11 @@ import Storage from "good-storage";
* storage handler * storage handler
*/ */
const SessionIDKey = process.env.VUE_APP_KEY_PREFIX + 'SESSION_ID';
const UserTokenKey = process.env.VUE_APP_KEY_PREFIX + "Authorization"; const UserTokenKey = process.env.VUE_APP_KEY_PREFIX + "Authorization";
const AdminTokenKey = process.env.VUE_APP_KEY_PREFIX + "Admin-Authorization" const AdminTokenKey = process.env.VUE_APP_KEY_PREFIX + "Admin-Authorization"
export function getSessionId() { export function getSessionId() {
let sessionId = Storage.get(SessionIDKey) return randString(42)
if (!sessionId) {
sessionId = randString(42)
setSessionId(sessionId)
}
return sessionId
}
export function removeSessionId() {
Storage.remove(SessionIDKey)
}
export function setSessionId(sessionId) {
Storage.set(SessionIDKey, sessionId)
} }
export function getUserToken() { export function getUserToken() {

View File

@ -286,7 +286,7 @@ const notice = ref("")
const noticeKey = ref("SYSTEM_NOTICE") const noticeKey = ref("SYSTEM_NOTICE")
if (isMobile()) { if (isMobile()) {
router.replace("/mobile") router.replace("/mobile/chat")
} }
// //

View File

@ -760,6 +760,7 @@ onMounted(() => {
}) })
onUnmounted(() => { onUnmounted(() => {
clipboard.value.destroy()
socket.value = null socket.value = null
}) })

View File

@ -1,13 +1,14 @@
<template> <template>
<van-config-provider :theme="getMobileTheme()"> <van-config-provider :theme="theme">
<div class="mobile-home"> <div class="mobile-home">
<router-view/> <router-view/>
<van-tabbar route v-model="active" @change="onChange"> <van-tabbar route v-model="active">
<van-tabbar-item to="/mobile/index" name="home" icon="home-o">首页</van-tabbar-item> <van-tabbar-item to="/mobile/index" name="home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/mobile/chat" name="chat" icon="chat-o">对话</van-tabbar-item> <van-tabbar-item to="/mobile/chat" name="chat" icon="chat-o">对话</van-tabbar-item>
<van-tabbar-item to="/mobile/image" name="image" icon="photo-o">绘图</van-tabbar-item> <van-tabbar-item to="/mobile/image" name="image" icon="photo-o">绘图</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的</van-tabbar-item> <van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的
</van-tabbar-item>
</van-tabbar> </van-tabbar>
</div> </div>
@ -17,9 +18,10 @@
<script setup> <script setup>
import {ref} from "vue"; import {ref} from "vue";
import {getMobileTheme} from "@/store/system"; import {getMobileTheme, setMobileTheme} from "@/store/system";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {isMobile} from "@/utils/libs"; import {isMobile} from "@/utils/libs";
import bus from '@/store/eventbus'
const router = useRouter() const router = useRouter()
if (!isMobile()) { if (!isMobile()) {
@ -27,9 +29,12 @@ if (!isMobile()) {
} }
const active = ref('home') const active = ref('home')
const onChange = (index) => { const theme = ref(getMobileTheme())
console.log(index)
} bus.on('changeTheme', (value) => {
theme.value = value
setMobileTheme(theme.value)
})
</script> </script>

View File

@ -30,7 +30,7 @@
<van-field label="有效期" v-if="form.expired_time > 0"> <van-field label="有效期" v-if="form.expired_time > 0">
<template #input> <template #input>
<van-tag type="warning">{{ dateFormat(form.expired_time) }}</van-tag> {{ dateFormat(form.expired_time) }}
</template> </template>
</van-field> </van-field>
@ -39,11 +39,15 @@
<div class="opt" v-if="isLogin"> <div class="opt" v-if="isLogin">
<van-row :gutter="10"> <van-row :gutter="10">
<van-col :span="12"> <van-col :span="8">
<van-button round block type="primary" @click="showPasswordDialog = true">修改密码</van-button> <van-button round block @click="showPasswordDialog = true" size="small">修改密码</van-button>
</van-col> </van-col>
<van-col :span="12"> <van-col :span="8">
<van-button round block @click="logout">退出登录</van-button> <van-button round block @click="logout" size="small">退出登录</van-button>
</van-col>
<van-col :span="8">
<van-button round block @click="showSettings = true" icon="setting" size="small">设置</van-button>
</van-col> </van-col>
</van-row> </van-row>
</div> </div>
@ -114,6 +118,34 @@
</van-cell-group> </van-cell-group>
</van-form> </van-form>
</van-dialog> </van-dialog>
<van-action-sheet v-model:show="showSettings" title="用户设置">
<div class="setting-content">
<van-form>
<van-cell-group inset>
<van-field name="switch" label="暗黑主题">
<template #input>
<van-switch v-model="dark" @change="changeTheme"/>
</template>
</van-field>
<!-- <van-field-->
<!-- v-model="password"-->
<!-- type="password"-->
<!-- name="密码"-->
<!-- label="密码"-->
<!-- placeholder="密码"-->
<!-- :rules="[{ required: true, message: '请填写密码' }]"-->
<!-- />-->
</van-cell-group>
<!-- <div style="margin: 16px;">-->
<!-- <van-button round block type="primary" native-type="submit">-->
<!-- 提交-->
<!-- </van-button>-->
<!-- </div>-->
</van-form>
</div>
</van-action-sheet>
</div> </div>
</template> </template>
@ -127,6 +159,8 @@ import {ElMessage} from "element-plus";
import {checkSession} from "@/action/session"; import {checkSession} from "@/action/session";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {removeUserToken} from "@/store/session"; import {removeUserToken} from "@/store/session";
import bus from '@/store/eventbus'
import {getMobileTheme} from "@/store/system";
const form = ref({ const form = ref({
username: 'GeekMaster', username: 'GeekMaster',
@ -148,6 +182,7 @@ const payWays = ref({})
const router = useRouter() const router = useRouter()
const userId = ref(0) const userId = ref(0)
const isLogin = ref(false) const isLogin = ref(false)
const showSettings = ref(false)
onMounted(() => { onMounted(() => {
checkSession().then(user => { checkSession().then(user => {
@ -276,6 +311,13 @@ const logout = function () {
showFailToast('注销失败!'); showFailToast('注销失败!');
}) })
} }
const dark = ref(getMobileTheme() === 'dark')
const changeTheme = () => {
bus.emit('changeTheme', dark.value ? 'dark' : 'light')
}
</script> </script>
<style lang="stylus"> <style lang="stylus">
@ -305,8 +347,10 @@ const logout = function () {
.product-list { .product-list {
padding 0 15px padding 0 15px
color var(--van-text-color)
.item { .item {
border 1px solid #e5e5e5 border 1px solid var(--van-border-color)
border-radius 10px border-radius 10px
margin-bottom 15px margin-bottom 15px
overflow hidden overflow hidden
@ -327,6 +371,10 @@ const logout = function () {
} }
} }
.van-cell__value {
flex 2
}
.price { .price {
font-size 18px font-size 18px
color #f56c6c color #f56c6c
@ -334,5 +382,9 @@ const logout = function () {
} }
} }
} }
.setting-content {
padding 16px
}
} }
</style> </style>

View File

@ -270,6 +270,7 @@
</div> </div>
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">复制</button>
</div> </div>
</template> </template>
@ -280,7 +281,6 @@ import {
showFailToast, showFailToast,
showNotify, showNotify,
showToast, showToast,
showDialog,
showImagePreview, showImagePreview,
showSuccessToast showSuccessToast
} from "vant"; } from "vant";
@ -291,6 +291,7 @@ import {checkSession} from "@/action/session";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {Delete} from "@element-plus/icons-vue"; import {Delete} from "@element-plus/icons-vue";
import {showLoginDialog} from "@/utils/libs"; import {showLoginDialog} from "@/utils/libs";
import Clipboard from "clipboard";
const activeColspan = ref([""]) const activeColspan = ref([""])
@ -337,8 +338,18 @@ const socket = ref(null)
const power = ref(0) const power = ref(0)
const activeName = ref("txt2img") const activeName = ref("txt2img")
const isLogin = ref(false) const isLogin = ref(false)
const prompt = ref('')
const clipboard = ref(null)
onMounted(() => { onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.value.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
checkSession().then(user => { checkSession().then(user => {
power.value = user['power'] power.value = user['power']
userId.value = user.id userId.value = user.id
@ -354,6 +365,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
socket.value = null socket.value = null
clipboard.value.destroy()
}) })
const mjPower = ref(1) const mjPower = ref(1)
@ -616,11 +628,15 @@ const publishImage = (item, action) => {
} }
const showPrompt = (item) => { const showPrompt = (item) => {
showDialog({ prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词", title: "绘画提示词",
message: item.prompt, message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
}).then(() => { }).then(() => {
// on close document.querySelector('#copy-btn').click()
}).catch(() => {
}); });
} }

View File

@ -198,7 +198,7 @@
<el-button type="success" v-else @click="publishImage($event, item, true)" circle> <el-button type="success" v-else @click="publishImage($event, item, true)" circle>
<i class="iconfont icon-share-bold"></i> <i class="iconfont icon-share-bold"></i>
</el-button> </el-button>
<el-button type="primary" @click="showTask(item)" circle> <el-button type="primary" @click="showPrompt(item)" circle>
<i class="iconfont icon-prompt"></i> <i class="iconfont icon-prompt"></i>
</el-button> </el-button>
</div> </div>
@ -208,7 +208,7 @@
</van-list> </van-list>
</div> </div>
<button style="display: none" class="copy-prompt-sd" :data-clipboard-text="prompt" id="copy-btn-sd">复制</button>
</div> </div>
</template> </template>
@ -232,7 +232,6 @@ import {showLoginDialog} from "@/utils/libs";
const listBoxHeight = ref(window.innerHeight - 40) const listBoxHeight = ref(window.innerHeight - 40)
const mjBoxHeight = ref(window.innerHeight - 150) const mjBoxHeight = ref(window.innerHeight - 150)
const showTaskDialog = ref(false)
const item = ref({}) const item = ref({})
const isLogin = ref(false) const isLogin = ref(false)
const activeColspan = ref([""]) const activeColspan = ref([""])
@ -338,15 +337,15 @@ const connect = () => {
} }
const clipboard = ref(null) const clipboard = ref(null)
const prompt = ref('')
onMounted(() => { onMounted(() => {
initData() initData()
clipboard.value = new Clipboard('.copy-prompt-sd'); clipboard.value = new Clipboard(".copy-prompt-sd");
clipboard.value.on('success', () => { clipboard.value.on('success', () => {
showNotify({type: "success", message: "复制成功!"}); showNotify({type: 'success', message: '复制成功', duration: 1000})
}) })
clipboard.value.on('error', () => { clipboard.value.on('error', () => {
showNotify({type: "danger", message: '复制失败!'}); showNotify({type: 'danger', message: '复制失败', duration: 2000})
}) })
httpGet("/api/config/get?key=system").then(res => { httpGet("/api/config/get?key=system").then(res => {
@ -438,7 +437,7 @@ const generate = () => {
return showToast("请输入绘画提示词!") return showToast("请输入绘画提示词!")
} }
if (params.value.seed === '') { if (!params.value.seed) {
params.value.seed = -1 params.value.seed = -1
} }
params.value.session_id = getSessionId() params.value.session_id = getSessionId()
@ -450,14 +449,17 @@ const generate = () => {
}) })
} }
const showTask = (row) => { const showPrompt = (item) => {
item.value = row prompt.value = item.prompt
showTaskDialog.value = true showConfirmDialog({
} title: "绘画提示词",
message: item.prompt,
const copyParams = (row) => { confirmButtonText: "复制",
params.value = row.params cancelButtonText: "关闭",
showTaskDialog.value = false }).then(() => {
document.querySelector('#copy-btn-sd').click()
}).catch(() => {
});
} }
const removeImage = (event, item) => { const removeImage = (event, item) => {

View File

@ -13,7 +13,13 @@
style="height: 100%;width: 100%;" style="height: 100%;width: 100%;"
> >
<van-cell v-for="item in data['mj'].data" :key="item.id"> <van-cell v-for="item in data['mj'].data" :key="item.id">
<van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/> <van-image :src="item['img_thumb']" @click="imageView(item)" fit="cover"/>
<div class="opt-box">
<el-button type="primary" @click="showPrompt(item)" circle>
<i class="iconfont icon-prompt"></i>
</el-button>
</div>
</van-cell> </van-cell>
</van-list> </van-list>
</van-tab> </van-tab>
@ -27,7 +33,13 @@
@load="onLoad" @load="onLoad"
> >
<van-cell v-for="item in data['sd'].data" :key="item.id"> <van-cell v-for="item in data['sd'].data" :key="item.id">
<van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/> <van-image :src="item['img_thumb']" @click="imageView(item)" fit="cover"/>
<div class="opt-box">
<el-button type="primary" @click="showPrompt(item)" circle>
<i class="iconfont icon-prompt"></i>
</el-button>
</div>
</van-cell> </van-cell>
</van-list> </van-list>
</van-tab> </van-tab>
@ -36,13 +48,19 @@
</van-tab> </van-tab>
</van-tabs> </van-tabs>
</div> </div>
<button style="display: none" class="copy-prompt-wall" :data-clipboard-text="prompt" id="copy-btn-wall">复制
</button>
</div> </div>
</template> </template>
<script setup> <script setup>
import {ref} from "vue"; import {onMounted, onUnmounted, ref} from "vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {showDialog, showFailToast} from "vant"; import {showConfirmDialog, showDialog, showFailToast, showImagePreview, showNotify} from "vant";
import {Delete} from "@element-plus/icons-vue";
import Clipboard from "clipboard";
import {ElMessage} from "element-plus";
const activeName = ref("mj") const activeName = ref("mj")
const data = ref({ const data = ref({
@ -52,7 +70,7 @@ const data = ref({
error: false, error: false,
page: 1, page: 1,
pageSize: 12, pageSize: 12,
url: "/api/mj/jobs", url: "/api/mj/imgWall",
data: [] data: []
}, },
"sd": { "sd": {
@ -61,7 +79,7 @@ const data = ref({
error: false, error: false,
page: 1, page: 1,
pageSize: 12, pageSize: 12,
url: "/api/sd/jobs", url: "/api/sd/imgWall",
data: [] data: []
}, },
"dalle3": { "dalle3": {
@ -70,11 +88,32 @@ const data = ref({
error: false, error: false,
page: 1, page: 1,
pageSize: 12, pageSize: 12,
url: "/api/dalle3/jobs", url: "/api/dalle3/imgWall",
data: [] data: []
} }
}) })
const prompt = ref('')
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt-wall");
clipboard.value.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.value.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
})
onUnmounted(() => {
clipboard.value.destroy()
})
const onLoad = () => { const onLoad = () => {
const d = data.value[activeName.value] const d = data.value[activeName.value]
httpGet(`${d.url}?status=1&page=${d.page}&page_size=${d.pageSize}&publish=true`).then(res => { httpGet(`${d.url}?status=1&page=${d.page}&page_size=${d.pageSize}&publish=true`).then(res => {
@ -105,22 +144,39 @@ const onLoad = () => {
}; };
const showPrompt = (item) => { const showPrompt = (item) => {
showDialog({ prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词", title: "绘画提示词",
message: item.prompt, message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
}).then(() => { }).then(() => {
// on close document.querySelector('#copy-btn-wall').click()
}).catch(() => {
}); });
} }
const imageView = (item) => {
showImagePreview([item['img_url']]);
}
</script> </script>
<style lang="stylus"> <style lang="stylus">
.img-wall { .img-wall {
.content { .content {
.van-cell__value { .van-cell__value {
min-height 80px
.van-image { .van-image {
width 100% width 100%
} }
.opt-box {
position absolute
right 0
top 0
padding 10px
}
} }
} }
} }