初始化
91
chat/src/App.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { NConfigProvider, NGlobalStyle, dateZhCN } from 'naive-ui'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ClientJS } from 'clientjs'
|
||||
import { ss } from './utils/storage'
|
||||
import { NaiveProvider } from '@/components/common'
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import { useLanguage } from '@/hooks/useLanguage'
|
||||
import { useAuthStore, useGlobalStoreWithOut, useChatStore } from '@/store'
|
||||
const client = new ClientJS()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
// Get the client's fingerprint id
|
||||
const fingerprint = client.getFingerprint()
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const router = useRouter()
|
||||
useGlobalStore.updateFingerprint(fingerprint)
|
||||
const { theme, lightThemeOverrides, darkThemeOverrides } = useTheme()
|
||||
const { language } = useLanguage()
|
||||
|
||||
const homePath = computed(() => authStore.globalConfig?.clientHomePath)
|
||||
const faviconPath = computed(() => authStore.globalConfig?.clientFavoIconPath || '/favicon.svg')
|
||||
const isAutoOpenNotice = computed(() => Number(authStore.globalConfig?.isAutoOpenNotice) === 1)
|
||||
|
||||
async function loadBaiduCode() {
|
||||
const baiduCode: any = authStore.globalConfig?.baiduCode || ''
|
||||
if (!baiduCode)
|
||||
return
|
||||
const scriptElem = document.createElement('script')
|
||||
const escapedCode = baiduCode.replace(/<script[\s\S]*?>([\s\S]*?)<\/script>/gi, '$1')
|
||||
scriptElem.innerHTML = escapedCode
|
||||
document.head.appendChild(scriptElem)
|
||||
}
|
||||
|
||||
function setDocumentTitle() {
|
||||
document.title = authStore.globalConfig?.siteName || 'AI'
|
||||
}
|
||||
|
||||
const themeOverrides = computed(() => {
|
||||
const config = !theme.value ? lightThemeOverrides : darkThemeOverrides
|
||||
return config
|
||||
})
|
||||
|
||||
function goHome() {
|
||||
homePath.value && router.push(homePath.value)
|
||||
}
|
||||
|
||||
function noticeInit() {
|
||||
const showNotice = ss.get('showNotice')
|
||||
if (!showNotice && isAutoOpenNotice.value) {
|
||||
useGlobalStore.updateNoticeDialog(true)
|
||||
}
|
||||
else {
|
||||
if (Date.now() > Number(showNotice) && isAutoOpenNotice.value)
|
||||
useGlobalStore.updateNoticeDialog(true)
|
||||
}
|
||||
}
|
||||
|
||||
/* 动态设置网站ico svg格式 */
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'shortcut icon'
|
||||
link.href = faviconPath.value
|
||||
link.type = 'image/svg+xml'
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
|
||||
onMounted(async () => {
|
||||
goHome()
|
||||
await chatStore.getBaseModelConfig()
|
||||
loadBaiduCode()
|
||||
setDocumentTitle()
|
||||
noticeInit()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NConfigProvider
|
||||
class="h-full "
|
||||
:theme="theme"
|
||||
:theme-overrides="themeOverrides"
|
||||
:locale="language"
|
||||
:date-locale="dateZhCN"
|
||||
preflight-style-disabled
|
||||
>
|
||||
<NaiveProvider>
|
||||
<RouterView />
|
||||
</NaiveProvider>
|
||||
<NGlobalStyle />
|
||||
</NConfigProvider>
|
||||
</template>
|
||||
44
chat/src/api/appStore.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* 查询app分组 */
|
||||
export function fetchQueryAppCatsAPI<T>(): Promise<T> {
|
||||
return get<T>({ url: '/app/queryCats' })
|
||||
}
|
||||
|
||||
/* 查询全量app列表 */
|
||||
export function fetchQueryAppsAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/app/list',
|
||||
})
|
||||
}
|
||||
|
||||
/* 查询个人app列表 */
|
||||
export function fetchQueryMineAppsAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/app/mineApps',
|
||||
})
|
||||
}
|
||||
|
||||
/* 收藏app */
|
||||
export function fetchCollectAppAPI<T>(data: { appId: number }): Promise<T> {
|
||||
return post<T>({ url: '/app/collect', data })
|
||||
}
|
||||
|
||||
/* 收藏app */
|
||||
export function fetchCustomAppAPI<T>(data: any): Promise<T> {
|
||||
return post<T>({ url: '/app/customApp', data })
|
||||
}
|
||||
|
||||
/* 删除app */
|
||||
export function fetchDelMineAppAPI<T>(data: any): Promise<T> {
|
||||
return post<T>({ url: '/app/delMineApp', data })
|
||||
}
|
||||
|
||||
/* 查询全量app列表 */
|
||||
export function fetchQueryOneCatAPI<T>(data): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/app/queryOneCat',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
30
chat/src/api/balance.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* get rechargeLog */
|
||||
export function fetchGetRechargeLogAPI<T>(data: { page?: number; size?: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/balance/rechargeLog',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* query balance */
|
||||
export function fetchGetBalanceQueryAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/balance/query',
|
||||
})
|
||||
}
|
||||
|
||||
/* log invite link count */
|
||||
export function fetchVisitorCountAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/balance/getVisitorCount',
|
||||
})
|
||||
}
|
||||
|
||||
/* log invite link count */
|
||||
export function fetchSyncVisitorDataAPI<T>(): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/balance/inheritVisitorData',
|
||||
})
|
||||
}
|
||||
33
chat/src/api/chatLog.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* 删除对话记录 */
|
||||
export function fetchDelChatLogAPI<T>(data: { id: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/chatlog/del',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 删除一组对话记录 */
|
||||
export function fetchDelChatLogByGroupIdAPI<T>(data: { groupId: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/chatlog/delByGroupId',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 查询x组对话信息 */
|
||||
export function fetchQueryChatLogListAPI<T>(data: { groupId: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/chatlog/chatList',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 查询单个应用的对话信息 */
|
||||
export function fetchQueryChatLogByAppIdAPI<T>(data: { page?: number; size?: number; appId: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/chatlog/byAppId',
|
||||
data,
|
||||
})
|
||||
}
|
||||
17
chat/src/api/config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/* query globle config */
|
||||
export function fetchQueryConfigAPI<T>(data: any) {
|
||||
return get<T>({
|
||||
url: '/config/queryFronet',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* query globle menu */
|
||||
export function fetchQueryMenuAPI<T>(data: any) {
|
||||
return get<T>({
|
||||
url: '/menu/list',
|
||||
data,
|
||||
})
|
||||
}
|
||||
17
chat/src/api/crami.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* use crami */
|
||||
export function fetchUseCramiAPI<T>(data: { code: string }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/crami/useCrami',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get all crami package */
|
||||
export function fetchGetPackageAPI<T>(data: { status: number; type?: number; size?: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/crami/queryAllPackage',
|
||||
data,
|
||||
})
|
||||
}
|
||||
8
chat/src/api/global.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/* get notice */
|
||||
export function fetchGetGlobalNoticeAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/config/notice',
|
||||
})
|
||||
}
|
||||
43
chat/src/api/group.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* 创建新的对话组 */
|
||||
export function fetchCreateGroupAPI<T>(data?: { appId?: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/group/create',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 查询对话组列表 */
|
||||
export function fetchQueryGroupAPI<T>(): Promise<T> {
|
||||
return get<T>({ url: '/group/query' })
|
||||
}
|
||||
|
||||
/* 修改对话组 */
|
||||
export function fetchUpdateGroupAPI<T>(data?: {
|
||||
groupId?: number
|
||||
title?: string
|
||||
isSticky?: boolean,
|
||||
config?: string
|
||||
}): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/group/update',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 删除对话组 */
|
||||
export function fetchDelGroupAPI<T>(data?: { groupId: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/group/del',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 删除全部对话组 */
|
||||
export function fetchDelAllGroupAPI<T>(data?: { groupId: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/group/delAll',
|
||||
data,
|
||||
})
|
||||
}
|
||||
163
chat/src/api/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
|
||||
import { get, post } from '@/utils/request'
|
||||
import { useSettingStore } from '@/store'
|
||||
|
||||
/* 流失对话聊天 */
|
||||
export function fetchChatAPIProcess<T = any>(
|
||||
params: {
|
||||
prompt: string
|
||||
appId?: number
|
||||
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
|
||||
signal?: GenericAbortSignal
|
||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
|
||||
) {
|
||||
return post<T>({
|
||||
url: '/chatgpt/chat-process',
|
||||
data: { prompt: params.prompt, appId: params?.appId, options: params.options },
|
||||
signal: params.signal,
|
||||
onDownloadProgress: params.onDownloadProgress,
|
||||
})
|
||||
}
|
||||
|
||||
/* 获取个人信息 */
|
||||
export function fetchGetInfo<T>() {
|
||||
return get<T>({ url: '/auth/getInfo' })
|
||||
}
|
||||
|
||||
/* 注册 */
|
||||
export function fetchRegisterAPI<T>(data: { username: string;password: string;email: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/register', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 注册 */
|
||||
export function fetchRegisterByPhoneAPI<T>(data: { username: string;password: string; phone: string; phoneCode: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/registerByPhone', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 登录 */
|
||||
export function fetchLoginAPI<T>(data: { username: string; password: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/login', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 手机号登录 */
|
||||
export function fetchLoginByPhoneAPI<T>(data: { phone: string; password: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/loginByPhone', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 修改个人信息 */
|
||||
export function fetchUpdateInfoAPI<T>(data: { username?: string; avatar?: string }): Promise<T> {
|
||||
return post<T>({ url: '/user/update', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取个人绘画记录 */
|
||||
export function fetchGetChatLogDraw<T>(data: { model: string }): Promise<T> {
|
||||
return get<T>({ url: '/chatLog/draw', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取所有绘画记录 */
|
||||
export function fetchGetAllChatLogDraw<T>(data: { size: number; rec: number; model: string }): Promise<T> {
|
||||
return get<T>({ url: '/chatLog/drawAll', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* chatgpt的dall-e2绘画 */
|
||||
export function fetchChatDraw<T>(data: { prompt: string;n: number;size: string }): Promise<T> {
|
||||
return post<T>({ url: '/chatgpt/chat-draw', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 修改密码 */
|
||||
export function fetchUpdatePasswordAPI<T>(data: { oldPassword?: string;password?: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/updatePassword', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 同步对话 */
|
||||
export function fetchGetchatSyncApi<T = any>(
|
||||
params: {
|
||||
prompt: string
|
||||
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
|
||||
signal?: GenericAbortSignal
|
||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
|
||||
) {
|
||||
return post<T>({
|
||||
url: '/chatgpt/chat-sync',
|
||||
data: { prompt: params.prompt, options: params.options },
|
||||
signal: params.signal,
|
||||
onDownloadProgress: params.onDownloadProgress,
|
||||
})
|
||||
}
|
||||
|
||||
/* 获取mind绘画联想词 */
|
||||
export function fetchGetchatMindApi<T = any>(
|
||||
params: {
|
||||
prompt: string
|
||||
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
|
||||
signal?: GenericAbortSignal
|
||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
|
||||
) {
|
||||
return post<T>({
|
||||
url: '/chatgpt/chat-mind',
|
||||
data: { prompt: params.prompt, options: params.options },
|
||||
signal: params.signal,
|
||||
onDownloadProgress: params.onDownloadProgress,
|
||||
})
|
||||
}
|
||||
|
||||
/* 获取MJ绘画联想词 */
|
||||
export function fetchGetMjPromptAssociateApi<T>(data: { prompt: string }): Promise<T> {
|
||||
return post<T>({ url: '/chatgpt/mj-associate', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取MJ绘画联想词 */
|
||||
export function fetchGetMjPromptFanyiApi<T>(data: { prompt: string }): Promise<T> {
|
||||
return post<T>({ url: '/chatgpt/mj-fy', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取我得绘制列表 */
|
||||
export function fetchMidjourneyDrawList<T>(data: { page?: number; size?: number }): Promise<T> {
|
||||
return get<T>({ url: '/midjourney/drawList', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取Mj提示词 */
|
||||
export function fetchMidjourneyPromptList<T>(): Promise<T> {
|
||||
return get<T>({ url: '/midjourney/queryPrompts' }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取Mj完整提示词 */
|
||||
export function fetchMidjourneyFullPrompt<T>(data: any): Promise<T> {
|
||||
return get<T>({ url: '/midjourney/getFullPrompt', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 删除MJ绘画记录 */
|
||||
export function fetchDownloadImg<T>(data: { id: number }): Promise<T> {
|
||||
return post<T>({ url: '/midjourney/delete', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取我得绘制列表 */
|
||||
export function fetchMidjourneyGetList<T>(data: { page?: number; size?: number; rec: number }): Promise<T> {
|
||||
return get<T>({ url: '/midjourney/getList', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 推荐图片 */
|
||||
export function fetchRecDraw<T>(data: { id: number }): Promise<T> {
|
||||
return post<T>({ url: '/midjourney/rec', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取图片验证码 */
|
||||
export function fetchCaptchaImg<T>(data: { color: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/captcha', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 发送手机验证码 */
|
||||
export function fetchSendSms<T>(data: { phone: string; captchaId: string; captchaCode: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/sendPhoneCode', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取九宫格设置 */
|
||||
export function fetchGetChatBoxList<T>() {
|
||||
return get<T>({ url: '/chatgpt/queryChatBoxFrontend' })
|
||||
}
|
||||
|
||||
|
||||
/* 获取快问设置 */
|
||||
export function fetchGetChatPreList<T>() {
|
||||
return get<T>({ url: '/chatgpt/queryChatPreList' })
|
||||
}
|
||||
52
chat/src/api/mjDraw.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* mj draw */
|
||||
export function fetchMjDtawAPI<T>(data: { prompt: string }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/mj/draw',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* mj upscale Img */
|
||||
export function fetchUpscaleSingleImgAPI<T>(data: { message_id: string; orderId: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/mj/upscaleSingleImg',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* mj variation img */
|
||||
export function fetchVariationSingleImgAPI<T>(data: { message_id: string; orderId: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/mj/variationSingleImg',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* mj fanyi */
|
||||
export function fetchTranslateAPI<T>(data: { text: string }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/fanyi/translate',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 提交一个绘画任务 */
|
||||
export function fetchDrawTaskAPI<T>(data: { prompt?: string; imgUrl?: string; extraParam?: string; drawId?: number; action?: number; orderId?: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/queue/addMjDrawQueue',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 代理图片 */
|
||||
export function fetchProxyImgAPI<T>(data: { url: string }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/midjourney/proxy',
|
||||
data,
|
||||
headers: {
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
})
|
||||
}
|
||||
15
chat/src/api/models.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/* query models list */
|
||||
export function fetchQueryModelsListAPI<T>() {
|
||||
return get<T>({
|
||||
url: '/models/list',
|
||||
})
|
||||
}
|
||||
|
||||
/* query base model config */
|
||||
export function fetchModelBaseConfigAPI<T>() {
|
||||
return get<T>({
|
||||
url: '/models/baseConfig',
|
||||
})
|
||||
}
|
||||
17
chat/src/api/order.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* order buy */
|
||||
export function fetchOrderBuyAPI<T>(data: { goodsId: number; payType?: string }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/order/buy',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* order query */
|
||||
export function fetchOrderQueryAPI<T>(data: { orderId: string }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/order/queryByOrderId',
|
||||
data,
|
||||
})
|
||||
}
|
||||
37
chat/src/api/sales.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* query sales account */
|
||||
export function fetchSalesAccountAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/sales/mineAccount',
|
||||
})
|
||||
}
|
||||
|
||||
/* query sales records */
|
||||
export function fetchSalesRecordsAPI<T>(data: { page?: number; size?: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/sales/mineRecords',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* query sales order */
|
||||
export function fetchSalesOrderAPI<T>(data: { page?: number; size?: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/sales/drawMoneyOrder',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* salce appfor money */
|
||||
export function fetchAppforMoneyAPI<T>(data: {
|
||||
withdrawalAmount: number | null
|
||||
withdrawalChannels: number | null
|
||||
contactInformation: string
|
||||
remark?: string
|
||||
}): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/sales/appForMoney',
|
||||
data,
|
||||
})
|
||||
}
|
||||
15
chat/src/api/signin.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* sign in */
|
||||
export function fetchSignInAPI<T>(): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/signin/sign',
|
||||
})
|
||||
}
|
||||
|
||||
/* sign log */
|
||||
export function fetchSignLogAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/signin/signinLog',
|
||||
})
|
||||
}
|
||||
5
chat/src/api/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ResData {
|
||||
success: boolean
|
||||
message: string
|
||||
data: any
|
||||
}
|
||||
104
chat/src/api/user.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* gen inviteCode */
|
||||
export function fetchGenInviteCodeAPI<T>(): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/user/genInviteCode',
|
||||
})
|
||||
}
|
||||
|
||||
/* get inviteRecord */
|
||||
export function fetchGetInviteRecordAPI<T>(data: { page?: number; size?: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/user/inviteRecord',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wechat-login senceStr */
|
||||
export function fetchGetQRSceneStrAPI<T>(
|
||||
data: { invitedBy?: string },
|
||||
): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/official/getQRSceneStr',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wechat-login qr url */
|
||||
export function fetchGetQRCodeAPI<T>(
|
||||
data: { sceneStr: string },
|
||||
): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/official/getQRCode',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* login by scenceStr */
|
||||
export function fetchLoginBySceneStrAPI<T>(
|
||||
data: { sceneStr: string },
|
||||
): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/official/loginBySceneStr',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* login by code */
|
||||
export function fetchLoginByCodeAPI<T>(
|
||||
data: { code: string },
|
||||
): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/official/loginByCode',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wx registery config */
|
||||
export function fetchGetJsapiTicketAPI<T>(
|
||||
data: { url: string },
|
||||
): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/official/getJsapiTicket',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wechat-login senceStr */
|
||||
export function fetchGetQRSceneStrByBindAPI<T>(): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/official/getQRSceneStrByBind',
|
||||
})
|
||||
}
|
||||
|
||||
/* bind wx by scenceStr */
|
||||
export function fetchBindWxBySceneStrAPI<T>(
|
||||
data: { sceneStr: string },
|
||||
): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/official/bindWxBySceneStr',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wx rediriect login url */
|
||||
export function fetchWxLoginRedirectAPI<T>(
|
||||
data: { url: string },
|
||||
): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/official/getRedirectUrl',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* log invite link count */
|
||||
export function fetchInviteCodeAPI<T>(
|
||||
data: { code: string },
|
||||
): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/user/inviteLink',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
BIN
chat/src/assets/alipay.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
chat/src/assets/avatar.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
chat/src/assets/avatar_old.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
chat/src/assets/badge.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
chat/src/assets/fail.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
1
chat/src/assets/icons/draw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#292F33" d="M3.651 29.852L29.926 3.576c.391-.391 2.888 2.107 2.497 2.497L6.148 32.349c-.39.391-2.888-2.107-2.497-2.497z"/><path fill="#66757F" d="M30.442 4.051L4.146 30.347l.883.883L31.325 4.934z"/><path fill="#E1E8ED" d="M34.546 2.537l-.412-.412-.671-.671c-.075-.075-.165-.123-.255-.169-.376-.194-.844-.146-1.159.169l-2.102 2.102.495.495.883.883 1.119 1.119 2.102-2.102c.391-.391.391-1.024 0-1.414zM5.029 31.23l-.883-.883-.495-.495-2.209 2.208c-.315.315-.363.783-.169 1.159.046.09.094.18.169.255l.671.671.412.412c.391.391 1.024.391 1.414 0l2.208-2.208-1.118-1.119z"/><path fill="#F5F8FA" d="M31.325 4.934l2.809-2.809-.671-.671c-.075-.075-.165-.123-.255-.169l-2.767 2.767.884.882zM4.146 30.347L1.273 33.22c.046.09.094.18.169.255l.671.671 2.916-2.916-.883-.883z"/><path d="M28.897 14.913l1.542-.571.6-2.2c.079-.29.343-.491.644-.491.3 0 .564.201.643.491l.6 2.2 1.542.571c.262.096.435.346.435.625s-.173.529-.435.625l-1.534.568-.605 2.415c-.074.296-.341.505-.646.505-.306 0-.573-.209-.647-.505l-.605-2.415-1.534-.568c-.262-.096-.435-.346-.435-.625 0-.278.173-.528.435-.625M11.961 5.285l2.61-.966.966-2.61c.16-.433.573-.72 1.035-.72.461 0 .874.287 1.035.72l.966 2.61 2.609.966c.434.161.721.573.721 1.035 0 .462-.287.874-.721 1.035l-2.609.966-.966 2.61c-.161.433-.574.72-1.035.72-.462 0-.875-.287-1.035-.72l-.966-2.61-2.61-.966c-.433-.161-.72-.573-.72-1.035.001-.462.288-.874.72-1.035M24.13 20.772l1.383-.512.512-1.382c.085-.229.304-.381.548-.381.244 0 .463.152.548.381l.512 1.382 1.382.512c.23.085.382.304.382.548 0 .245-.152.463-.382.548l-1.382.512-.512 1.382c-.085.229-.304.381-.548.381-.245 0-.463-.152-.548-.381l-.512-1.382-1.383-.512c-.229-.085-.381-.304-.381-.548 0-.245.152-.463.381-.548" fill="#FFAC33"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
chat/src/assets/icons/gift.png
Normal file
|
After Width: | Height: | Size: 713 B |
1
chat/src/assets/icons/model.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706003306703" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26508" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M484.118261 266.562783l-193.446957 101.776695a40.893217 40.893217 0 0 0-26.045217 39.41287v208.372869a36.730435 36.730435 0 0 0 20.301913 37.030957l206.436174 108.710956a39.991652 39.991652 0 0 0 40.626087 1.113044c22.405565-11.853913 211.656348-112.161391 211.656348-112.161391a36.10713 36.10713 0 0 0 15.582608-29.862957c0.367304-20.813913 0-213.23687 0-213.236869a36.964174 36.964174 0 0 0-18.654608-35.283479l-209.452522-110.825739a34.148174 34.148174 0 0 0-25.6-4.852869 39.168 39.168 0 0 0-11.130435 4.36313z" fill="#87B3FF" p-id="26509"></path><path d="M308.424348 406.984348l202.963478 101.665391 0.545391 213.214609-203.508869-107.163826z" fill="#D3E3FF" p-id="26510"></path><path d="M712.325565 415.376696l-163.550608 82.721391v47.749565l163.550608-82.721391z" fill="#186CFF" p-id="26511"></path><path d="M512 100.173913a411.826087 411.826087 0 1 0 411.826087 411.826087A412.293565 412.293565 0 0 0 512 100.173913m0-100.173913A512 512 0 1 1 0 512 512 512 0 0 1 512 0z" fill="#ECF3FF" p-id="26512"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
chat/src/assets/icons/modelSvg.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706003418289" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26651" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M0 0h1024v1024H0z" fill="#1296db" fill-opacity="0" p-id="26652"></path><path d="M512 65.6L898.6 288.8v446.5L512 958.5 125.4 735.2V288.8L512 65.6m0-65.6L68.5 256v512l443.4 256 443.4-256V256L512 0z" fill="#1296db" p-id="26653"></path><path d="M338.5 602.8l-0.4 141.1c0 1.8-1 3.5-2.6 4.4-1.6 1-3.6 1-5.2-0.1l-122.8-70.8c-1.6-0.9-2.6-2.7-2.6-4.5l0.4-140.9c0-1.8 1-3.6 2.6-4.5 1.6-0.9 3.6-0.9 5.2 0l122.7 70.9c1.7 0.9 2.7 2.6 2.7 4.4z m0.6-183.4l-0.4 141c0 1.8-1 3.6-2.6 4.5-1.6 0.9-3.5 0.9-5.1-0.1L208.2 494c-1.6-0.9-2.6-2.7-2.6-4.5l0.4-141c0-1.8 1-3.6 2.6-4.5 1.6-0.9 3.6-0.9 5.2 0L336.6 414.9c1.5 1 2.5 2.7 2.5 4.5zM498.2 695l-0.4 141c0 1.8-1 3.5-2.6 4.5-1.6 0.9-3.5 0.9-5.1 0l-122.8-70.9c-1.6-0.9-2.6-2.6-2.6-4.5l0.4-141c0-1.8 1-3.6 2.6-4.5 1.6-0.9 3.6-0.9 5.2 0l122.8 70.9c1.5 1 2.5 2.7 2.5 4.5z m187-92.2l0.4 141.1c0 4 4.4 6.4 7.8 4.4l122.8-70.8c1.6-0.9 2.6-2.7 2.6-4.5l-0.5-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.5 0.9-2.5 2.6-2.5 4.4z m-0.5-183.4l0.4 141c0 1.9 1 3.6 2.6 4.5 1.6 0.9 3.6 0.9 5.2-0.1L815.6 494c1.6-0.9 2.6-2.7 2.6-4.5l-0.4-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.5 1-2.5 2.7-2.5 4.5zM525.6 695l0.3 141c0 4 4.3 6.4 7.7 4.5l122.8-70.9c1.6-0.9 2.6-2.6 2.6-4.5l-0.4-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.5 1-2.4 2.7-2.4 4.5z m-10.4-394.5l121.9-70.9c1.6-0.9 2.6-2.6 2.6-4.5s-1-3.5-2.6-4.5l-122.7-70.8c-1.6-0.9-3.5-0.9-5.1 0l-122 70.9c-1.6 0.9-2.6 2.6-2.6 4.4 0 1.8 1 3.6 2.6 4.5l122.8 70.9c1.5 1 3.5 1 5.1 0z m-158.7 92.2l121.9-70.9c1.6-0.9 2.6-2.6 2.6-4.5 0-1.8-1-3.5-2.6-4.4L355.7 242c-1.6-1-3.6-1-5.2 0L228.6 312.9c-1.6 0.9-2.6 2.6-2.6 4.4 0 1.8 1 3.6 2.6 4.5l122.8 70.9c1.6 1 3.6 1 5.1 0z m316.2 6.8l126.3-73.4c1.7-0.9 2.7-2.7 2.7-4.7s-1-3.7-2.7-4.7l-127.2-73.4c-1.6-1-3.7-1-5.3 0l-126.3 73.4c-1.7 1-2.7 2.7-2.7 4.7 0 1.9 1 3.7 2.7 4.7l127.2 73.4c1.6 0.9 3.6 0.9 5.3 0z m-13.6 19.9l-0.4 141c0 1.8-1 3.5-2.6 4.5-1.6 0.9-3.6 0.9-5.2-0.1L528.2 494c-1.6-0.9-2.6-2.7-2.6-4.5l0.4-141c0-4 4.3-6.4 7.8-4.4L656.6 415c1.5 0.9 2.5 2.6 2.5 4.4z m-294.4 0l0.3 141c0 1.8 1 3.6 2.6 4.5s3.6 0.9 5.2 0L495.6 494c1.6-0.9 2.6-2.7 2.6-4.5l-0.3-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.6 1-2.6 2.7-2.6 4.5z m150.5 247l121.9-70.9c1.6-0.9 2.6-2.6 2.6-4.5 0-1.8-1-3.5-2.6-4.4l-122.7-70.9c-1.6-0.9-3.5-0.9-5.1 0l-122 70.9c-1.6 0.9-2.5 2.6-2.6 4.4 0 1.9 1 3.6 2.6 4.5l122.8 70.9c1.5 0.9 3.5 0.9 5.1 0z" fill="#1296db" p-id="26654"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
chat/src/assets/icons/zoom.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#9AAAB4" d="M27.388 24.642L24.56 27.47l-4.95-4.95 2.828-2.828z"/><path fill="#66757F" d="M34.683 29.11l-5.879-5.879c-.781-.781-2.047-.781-2.828 0l-2.828 2.828c-.781.781-.781 2.047 0 2.828l5.879 5.879c1.562 1.563 4.096 1.563 5.658 0 1.56-1.561 1.559-4.094-.002-5.656z"/><circle fill="#8899A6" cx="13.586" cy="13.669" r="13.5"/><circle fill="#BBDDF5" cx="13.586" cy="13.669" r="9.5"/></svg>
|
||||
|
After Width: | Height: | Size: 460 B |
BIN
chat/src/assets/images/empty.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
chat/src/assets/images/mj.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
chat/src/assets/images/niji.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
chat/src/assets/images/preferential.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
chat/src/assets/img-bg.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
chat/src/assets/login-banner.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
chat/src/assets/market.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
chat/src/assets/qianbao.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
1
chat/src/assets/recommend.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
BIN
chat/src/assets/wechat.png
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
chat/src/assets/wxpay.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
130
chat/src/components/base/Loading.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gap: 10,
|
||||
progress: 0,
|
||||
tips: '',
|
||||
words: ['L', 'O', 'A', 'D', 'I', 'N', 'G']
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const theme = computed(() => appStore.theme)
|
||||
const loadingTextColor = computed(() => theme.value === 'dark' ? '#fff' : '#000')
|
||||
|
||||
interface Props {
|
||||
gap?: number
|
||||
progress?: number
|
||||
tips?: string
|
||||
bgColor?: string
|
||||
words?: any
|
||||
}
|
||||
// const words = ref<string[]>(['L', 'O', 'A', 'D', 'I', 'N', 'G'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="loading" :style="{ background: props.bgColor }">
|
||||
<div class="loading-text">
|
||||
<span v-for="item in props.words" :key="item" :style="{ margin: `0 ${props.gap}px`, color: loadingTextColor }" class="loading-text-words">{{ item }}</span>
|
||||
</div>
|
||||
<div v-if="!tips && props.progress" class="progress">
|
||||
绘制进度: {{ props.progress }}%
|
||||
</div>
|
||||
<div v-if="tips" class="progress">
|
||||
{{ props.tips }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.progress{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 110px;
|
||||
line-height: 100px;
|
||||
}
|
||||
.loading-text span {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
color: #fff;
|
||||
font-family: "Quattrocento Sans", sans-serif;
|
||||
}
|
||||
.loading-text span:nth-child(1) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(2) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.2s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.2s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(3) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.4s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.4s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(4) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.6s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.6s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(5) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 0.8s infinite linear alternate;
|
||||
animation: blur-text 1.5s 0.8s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(6) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 1s infinite linear alternate;
|
||||
animation: blur-text 1.5s 1s infinite linear alternate;
|
||||
}
|
||||
.loading-text span:nth-child(7) {
|
||||
filter: blur(0px);
|
||||
-webkit-animation: blur-text 1.5s 1.2s infinite linear alternate;
|
||||
animation: blur-text 1.5s 1.2s infinite linear alternate;
|
||||
}
|
||||
|
||||
@-webkit-keyframes blur-text {
|
||||
0% {
|
||||
filter: blur(0px);
|
||||
}
|
||||
100% {
|
||||
filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blur-text {
|
||||
0% {
|
||||
filter: blur(0px);
|
||||
}
|
||||
100% {
|
||||
filter: blur(4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
chat/src/components/base/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import TitleBar from './titleBar.vue'
|
||||
|
||||
export { TitleBar }
|
||||
164
chat/src/components/base/macTablebar.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useIpcRenderer } from '@vueuse/electron'
|
||||
defineProps<{ title?: string }>()
|
||||
import { useGlobalStore } from '@/store'
|
||||
|
||||
|
||||
const ipcRenderer = useIpcRenderer()
|
||||
const isFullScreen = ref(false)
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const checkIfWindowIsMaximized = () => {
|
||||
ipcRenderer.send('check-window-maximized');
|
||||
};
|
||||
|
||||
const handleMaximizedStatus: any = (_: Event, isMaximized: any) => {
|
||||
isFullScreen.value = isMaximized
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
ipcRenderer.on('window-maximized-status', handleMaximizedStatus);
|
||||
ipcRenderer.on('clipboard-content', clipboardHandle);
|
||||
checkIfWindowIsMaximized();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ipcRenderer.removeListener('window-maximized-status', handleMaximizedStatus);
|
||||
});
|
||||
|
||||
/* 关闭窗口 */
|
||||
const closeWindow = () => {
|
||||
ipcRenderer.invoke('closeWindow')
|
||||
}
|
||||
|
||||
/* 最大化最小化窗口 */
|
||||
const maxmizeMainWin = () => {
|
||||
ipcRenderer.invoke( isFullScreen.value ? 'unmaximizeWindow' : 'maxmizeWindow')
|
||||
isFullScreen.value = !isFullScreen.value
|
||||
}
|
||||
|
||||
/* 最小化窗口 */
|
||||
const minimizeMainWindow = () => {
|
||||
ipcRenderer.invoke('minimizeWindow')
|
||||
}
|
||||
|
||||
/* 处理粘贴内容 */
|
||||
const clipboardHandle = (event: any, content: any) => {
|
||||
globalStore.updateClipboardText(content)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="btn close-btn" @click="closeWindow" />
|
||||
<div v-if="isFullScreen" class="btn disabled" />
|
||||
<div v-if="!isFullScreen" class="btn min-btn" @click="minimizeMainWindow" />
|
||||
<div class="btn max-btn" @click="maxmizeMainWin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin-top: 8px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.btn:before,
|
||||
.btn:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: #FF5D5B;
|
||||
border: 1px solid #CF544D;
|
||||
}
|
||||
|
||||
.min-btn {
|
||||
background: #FFBB39;
|
||||
border: 1px solid #CFA64E;
|
||||
}
|
||||
|
||||
.disabled{
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
.max-btn {
|
||||
background: #00CD4E;
|
||||
border: 1px solid #0EA642;
|
||||
}
|
||||
|
||||
/* Close btn */
|
||||
.close-btn:before,
|
||||
.close-btn:after {
|
||||
width: 1px;
|
||||
height: 70%;
|
||||
background: #460100;
|
||||
}
|
||||
|
||||
.close-btn:before {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.close-btn:after {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* min btn */
|
||||
.min-btn:before {
|
||||
width: 70%;
|
||||
height: 1px;
|
||||
background: #460100;
|
||||
}
|
||||
|
||||
/* max btn */
|
||||
.max-btn:before {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background: #024D0F;
|
||||
}
|
||||
|
||||
.max-btn:after {
|
||||
width: 1px;
|
||||
height: 90%;
|
||||
transform: translate(-50%, -50%) rotate(-135deg);
|
||||
background: #00CD4E;
|
||||
}
|
||||
|
||||
/* Hover function */
|
||||
.wrapper:hover .btn:before,
|
||||
.wrapper:hover .btn:after {
|
||||
top: 50%;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
38
chat/src/components/base/titleBar.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { PlayBack } from '@vicons/ionicons5'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
des?: string
|
||||
padding?: number
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
des: '',
|
||||
padding: 4,
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
const darkMode = computed(() => appStore.theme === 'dark')
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex border-b border-[#ebebeb] dark:border-[#ffffff17] py-4 w-full" :class="[`px-${props.padding}`]">
|
||||
<div class="pt-1 mr-2 cursor-pointer">
|
||||
<NIcon size="16" class="text-primary" @click="router.push('/')">
|
||||
<PlayBack />
|
||||
</NIcon>
|
||||
</div>
|
||||
<div>
|
||||
<b :class="[darkMode ? 'text-[#fff]' : 'text-[#555]']" class="text-lg ">{{ props.title }}</b>
|
||||
<div :class="[darkMode ? 'text-[#fff]' : 'text-[#626569]']" class="text-truncate text-[#626569] mt-1">
|
||||
{{ props.des }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
235
chat/src/components/common/CanvasMask/index.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
/* 图片地址 如果不是同源跨域 传入base64 */
|
||||
src: String,
|
||||
/* 图片 高度 宽度 不传就是用图片宽高、如果是缩略图 使用尺寸导出到原始尺寸 */
|
||||
width: Number,
|
||||
height: Number,
|
||||
/* 允许的画布最大宽度 限制区域 */
|
||||
max: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
/* 导出蒙版的底色背景色 */
|
||||
exportMaskBackgroundColor: {
|
||||
type: String,
|
||||
default: 'black',
|
||||
},
|
||||
/* 导出蒙版的绘制颜色 */
|
||||
exportMaskColor: {
|
||||
type: String,
|
||||
default: 'white',
|
||||
},
|
||||
penColor: {
|
||||
type: String,
|
||||
default: 'white',
|
||||
},
|
||||
penWidth: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
updateFileInfo: Function,
|
||||
})
|
||||
|
||||
// TODO 如果动态变更了线宽颜色等 在导出的时候没有记录每一步的线宽 而是使用了最后的
|
||||
|
||||
const canvas = ref<any>(null)
|
||||
const backgroundCanvas = ref<any>(null)
|
||||
const paths = ref<any>([])
|
||||
let isDrawing = false
|
||||
let currentPath: any = []
|
||||
const baseImage: any = new Image()
|
||||
const isEraserEnabled = ref(false)
|
||||
|
||||
const computedWidth = ref(0)
|
||||
const computedHeight = ref(0)
|
||||
const scaleRatio = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
const ctx: any = canvas.value.getContext('2d')
|
||||
const backgroundCtx = backgroundCanvas.value?.getContext('2d')
|
||||
baseImage.src = props.src
|
||||
baseImage.onload = () => {
|
||||
const ratio = Math.min(props.max / baseImage.width, props.max / baseImage.height)
|
||||
scaleRatio.value = ratio
|
||||
computedWidth.value = props.width || (ratio < 1 ? baseImage.width * ratio : baseImage.width)
|
||||
computedHeight.value = props.height || (ratio < 1 ? baseImage.height * ratio : baseImage.height)
|
||||
props.updateFileInfo?.({
|
||||
width: baseImage.width,
|
||||
height: baseImage.height,
|
||||
scaleRatio: ratio.toFixed(3),
|
||||
})
|
||||
canvas.value.width = computedWidth.value
|
||||
backgroundCanvas.value.width = computedWidth.value
|
||||
canvas.value.height = computedHeight.value
|
||||
backgroundCanvas.value.height = computedHeight.value
|
||||
// ctx.drawImage(baseImage, 0, 0, computedWidth.value, computedHeight.value);
|
||||
backgroundCtx.drawImage(baseImage, 0, 0, computedWidth.value, computedHeight.value)
|
||||
}
|
||||
canvas.value.addEventListener('mousedown', startDrawing)
|
||||
canvas.value.addEventListener('mousemove', draw)
|
||||
canvas.value.addEventListener('mouseup', stopDrawing)
|
||||
})
|
||||
|
||||
/* 开始绘制 */
|
||||
const startDrawing = (e: any) => {
|
||||
isDrawing = true
|
||||
const ctx = canvas.value.getContext('2d')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(e.offsetX, e.offsetY)
|
||||
currentPath = [{ type: isEraserEnabled.value ? 'erase' : 'draw', x: e.offsetX, y: e.offsetY }]
|
||||
}
|
||||
|
||||
/* 绘制过程 */
|
||||
const draw = (e: any) => {
|
||||
if (!isDrawing)
|
||||
return
|
||||
const ctx = canvas.value.getContext('2d')
|
||||
ctx.lineTo(e.offsetX, e.offsetY)
|
||||
|
||||
if (isEraserEnabled.value) {
|
||||
// 橡皮擦模式:清除画布上的内容
|
||||
ctx.globalCompositeOperation = 'destination-out'
|
||||
ctx.lineWidth = props.penWidth * 2 // 橡皮擦宽度可以调整
|
||||
}
|
||||
else {
|
||||
// 正常绘制模式
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.strokeStyle = props.penColor
|
||||
ctx.lineWidth = props.penWidth
|
||||
}
|
||||
ctx.stroke()
|
||||
currentPath.push({ type: isEraserEnabled.value ? 'erase' : 'draw', x: e.offsetX, y: e.offsetY })
|
||||
}
|
||||
/* 完成单次绘制 */
|
||||
const stopDrawing = () => {
|
||||
isDrawing = false
|
||||
paths.value.push([...currentPath, { type: 'end' }])
|
||||
currentPath = []
|
||||
}
|
||||
|
||||
/* 获取Base图片 */
|
||||
const exportImage = (): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const exportCanvas = document.createElement('canvas')
|
||||
const image: any = baseImage
|
||||
exportCanvas.width = image.width
|
||||
exportCanvas.height = image.height
|
||||
const exportCtx = exportCanvas.getContext('2d')
|
||||
if (exportCtx) {
|
||||
exportCtx.fillStyle = props.exportMaskBackgroundColor
|
||||
exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height)
|
||||
exportCtx.beginPath()
|
||||
const xRatio = image.width / computedWidth.value
|
||||
const yRatio = image.height / computedHeight.value
|
||||
exportCtx.beginPath()
|
||||
paths.value.forEach((pathArr: any[]) => {
|
||||
pathArr.forEach((path, index) => {
|
||||
if (path.type === 'begin' || path.type === 'draw') {
|
||||
if (index === 0 || pathArr[index - 1].type !== path.type)
|
||||
exportCtx.beginPath()
|
||||
|
||||
exportCtx.lineTo(path.x * xRatio, path.y * yRatio)
|
||||
exportCtx.strokeStyle = props.exportMaskColor
|
||||
exportCtx.lineWidth = props.penWidth * xRatio
|
||||
}
|
||||
if (path.type === 'erase') {
|
||||
if (index === 0 || pathArr[index - 1].type !== path.type)
|
||||
exportCtx.beginPath()
|
||||
|
||||
exportCtx.lineTo(path.x * xRatio, path.y * yRatio)
|
||||
exportCtx.strokeStyle = props.exportMaskBackgroundColor // 擦除路径使用的颜色(黑色)
|
||||
}
|
||||
// 每当一个 'draw' 或 'erase' 类型的路径结束时,结束当前的路径
|
||||
if (index < pathArr.length - 1 && pathArr[index + 1].type !== path.type)
|
||||
exportCtx.stroke()
|
||||
})
|
||||
// 如果最后一个路径元素是 'draw' 或 'erase',确保路径被结束
|
||||
if (pathArr[pathArr.length - 1].type !== 'begin')
|
||||
exportCtx.stroke()
|
||||
})
|
||||
const base64Image = exportCanvas.toDataURL('image/png')
|
||||
resolve(base64Image)
|
||||
}
|
||||
else {
|
||||
reject(new Error('无法获取canvas的2D渲染上下文'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 清空画布并重置 */
|
||||
function clear() {
|
||||
paths.value = []
|
||||
const ctx = canvas.value.getContext('2d')
|
||||
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
|
||||
}
|
||||
|
||||
/* 获取绘制后的蒙版图片 */
|
||||
async function getBase() {
|
||||
return await exportImage()
|
||||
}
|
||||
|
||||
/* 返回上一步 */
|
||||
function undo() {
|
||||
if (paths.value.length > 0) {
|
||||
paths.value.pop()
|
||||
redrawCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
/* 重新绘制 */
|
||||
function redrawCanvas() {
|
||||
const ctx = canvas.value.getContext('2d')
|
||||
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
|
||||
ctx.drawImage(baseImage, 0, 0, computedWidth.value, computedHeight.value)
|
||||
|
||||
paths.value.forEach((pathArr: any[]) => {
|
||||
pathArr.forEach((path, index) => {
|
||||
if (index === 0 || pathArr[index - 1].type !== path.type)
|
||||
ctx.beginPath()
|
||||
|
||||
if (path.type === 'erase') {
|
||||
ctx.globalCompositeOperation = 'destination-out'
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0)'
|
||||
}
|
||||
else {
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
ctx.strokeStyle = 'white'
|
||||
}
|
||||
ctx.lineWidth = path.type === 'erase' ? props.penWidth * 2 : props.penWidth
|
||||
ctx.lineTo(path.x, path.y)
|
||||
ctx.stroke()
|
||||
if (index === pathArr.length - 1 || pathArr[index + 1].type !== path.type)
|
||||
ctx.closePath()
|
||||
})
|
||||
})
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
}
|
||||
|
||||
/* 切换橡皮擦模式 */
|
||||
const toggleEraser = () => {
|
||||
isEraserEnabled.value = !isEraserEnabled.value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getBase,
|
||||
undo,
|
||||
clear,
|
||||
toggleEraser,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full ">
|
||||
<canvas ref="backgroundCanvas" class="absolute left-0 top-0" :width="width" :height="height" />
|
||||
<canvas ref="canvas" class="absolute left-0 top-0" :width="width" :height="height" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
canvas {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
||||
338
chat/src/components/common/GridManager/index.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, computed, onUnmounted, getCurrentInstance, watch, nextTick } from 'vue'
|
||||
import { throttle } from '@/utils/functions/throttle'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { copyText } from '@/utils/format'
|
||||
import { useMessage, NPopover } from 'naive-ui'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { useRouter } from 'vue-router'
|
||||
const authStore = useAuthStore()
|
||||
interface Props {
|
||||
dataList: FileItem[]
|
||||
scaleWidth?: number
|
||||
isDrawLike?: boolean
|
||||
usePropmpt?: boolean
|
||||
copyPropmpt?: boolean
|
||||
gap?: number
|
||||
preOrigin?: boolean
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
width: number
|
||||
height: number
|
||||
cosUrl: string
|
||||
thumbImg: string
|
||||
size: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
interface FileItem {
|
||||
id: number
|
||||
fileInfo: FileInfo
|
||||
prompt: string
|
||||
fullPrompt?: string
|
||||
originUrl?: string
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(ev: 'loadMore'): void
|
||||
(ev: 'usePropmptDraw', prompt: string): void
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(),{
|
||||
gap: 5,
|
||||
})
|
||||
const emit = defineEmits<Emit>()
|
||||
const $viewerApi = getCurrentInstance()?.appContext.config.globalProperties.$viewerApi
|
||||
const ms = useMessage()
|
||||
const boxRefs = ref<any>({})
|
||||
const otherInfoContainerHeight = ref(0)
|
||||
const realWidth = ref(160)
|
||||
const realColumn = ref(0)
|
||||
const loadComplete = ref<number[]>([])
|
||||
const wapperRef = ref<HTMLDivElement | null>(null)
|
||||
const wapperHeigth = ref(0)
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
const width = computed(() => {
|
||||
return props.scaleWidth? Number(props.scaleWidth) * 2 + props.gap + 150 : 150
|
||||
});
|
||||
|
||||
const router = useRouter()
|
||||
/* 拿到图片高度 对定位top和right 新的一轮去插入最小值的那一列 贪心算法即可 */
|
||||
function compilerContainer() {
|
||||
calcHeight()
|
||||
compilerColumn()
|
||||
const columns = realColumn.value
|
||||
const itemWidth = realWidth.value
|
||||
const cacheHeight = <any>[]
|
||||
props.dataList.forEach((item, index) => {
|
||||
const { width, height } = item.fileInfo
|
||||
const bi = itemWidth / width
|
||||
const boxheight = height * bi + props.gap + otherInfoContainerHeight.value
|
||||
const currentBox = boxRefs.value[item.id]
|
||||
if (cacheHeight.length < columns) {
|
||||
currentBox.style.top = '0px'
|
||||
currentBox.style.left = `${(itemWidth + props.gap) * index}px`
|
||||
cacheHeight.push(boxheight)
|
||||
} else {
|
||||
const minHeight = Math.min.apply(null, cacheHeight)
|
||||
const minIndex = cacheHeight.findIndex((t: number) => t === minHeight)
|
||||
currentBox.style.top = `${minHeight + 0}px`
|
||||
currentBox.style.left = `${minIndex * (realWidth.value + props.gap)}px`
|
||||
cacheHeight[minIndex] += boxheight
|
||||
|
||||
}
|
||||
})
|
||||
wapperHeigth.value = Math.max(...cacheHeight) + 100
|
||||
}
|
||||
|
||||
function setItemRefs(el: HTMLDivElement, item: FileItem) {
|
||||
if (el && item) {
|
||||
boxRefs.value[item.id] = el;
|
||||
}
|
||||
}
|
||||
|
||||
/* 通过额外展示的信息计算有没有除了图片意外额外的高度 eg: 图片100px 额外显示其他信息30px cacheHeight的高度在图片的基础上需要+30 */
|
||||
function calcHeight() {
|
||||
const { showName = 0, showOther = 0 } = {}
|
||||
otherInfoContainerHeight.value = [showName, showOther].filter(t => t).length * 15
|
||||
}
|
||||
|
||||
|
||||
watch(() => props.scaleWidth, (val) => {
|
||||
handleResizeThrottled()
|
||||
})
|
||||
|
||||
watch(() => props.dataList, (val) => {
|
||||
if (!val) return;
|
||||
nextTick(() => {
|
||||
handleResizeThrottled()
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
/* 计算放多少列比较合理,并计算最终单个图片的宽 */
|
||||
function compilerColumn() {
|
||||
if (!wapperRef.value)
|
||||
return
|
||||
const containerWidth = wapperRef.value.clientWidth
|
||||
|
||||
/* 计算按目前宽度最多可以是几列 */
|
||||
realColumn.value = Math.floor(containerWidth / width.value)
|
||||
const surplus = containerWidth - realColumn.value * width.value // 剩下的多余空间
|
||||
/* 计算如果给了左右间距那么作业间距需要占多少宽度 */
|
||||
const positionWith = ((realColumn.value - 1) * props.gap) // 设置的right 需要padding的值
|
||||
/* 总宽度减去right的宽度,如果是负数考虑要不要cloumn-1 那么图片真实宽度就会比传入的宽度大 */
|
||||
if (surplus - positionWith < 0) {
|
||||
realColumn.value -= 1
|
||||
}
|
||||
/* 图片宽度*列 + right的间距 不管大于小于总宽 多的或者少的那部分都平分给列容器 保证总宽是100% */
|
||||
realWidth.value = Math.floor((containerWidth - positionWith) / realColumn.value)
|
||||
}
|
||||
|
||||
function imgLoadSuccess(e: any, item: FileItem) {
|
||||
loadComplete.value.push(item.id)
|
||||
}
|
||||
|
||||
function imgLoadError(e: any, item: FileItem) {
|
||||
loadComplete.value.push(item.id)
|
||||
}
|
||||
|
||||
function handleCopy(item: any) {
|
||||
if (!isLogin.value) {
|
||||
return authStore.setLoginDialog(true)
|
||||
}
|
||||
const { prompt } = item
|
||||
copyText({ text: prompt })
|
||||
ms.success('复制prompt成功')
|
||||
}
|
||||
|
||||
function drawLike(item: any) {
|
||||
router.push(`/midjourney?mjId=${item.id}`)
|
||||
}
|
||||
|
||||
|
||||
function usePropmptDraw(item: FileItem){
|
||||
const { prompt } = item
|
||||
emit('usePropmptDraw', prompt)
|
||||
}
|
||||
|
||||
|
||||
function handlePreview(item: any) {
|
||||
const { fileInfo } = item
|
||||
const { cosUrl } = fileInfo
|
||||
$viewerApi({ options: {}, images: [cosUrl] })
|
||||
}
|
||||
|
||||
const realHeight = computed(() => (item) => {
|
||||
const { fileInfo } = item
|
||||
const { width, height } = fileInfo
|
||||
return height / (width / realWidth.value)
|
||||
})
|
||||
|
||||
const handleResizeThrottled = throttle(function (this: any) {
|
||||
compilerContainer()
|
||||
}, 200)
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', handleResizeThrottled)
|
||||
const container: any = document.getElementById('footer')
|
||||
const observer = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
emit('loadMore')
|
||||
}
|
||||
})
|
||||
})
|
||||
observer.observe(container)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResizeThrottled)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class=" min-h-full overflow-hidden flex flex-col">
|
||||
<div class="flex-1 min-h-full p-4 relative">
|
||||
<div id="wapper" ref="wapperRef" class="wapper" :style="{ height: `${wapperHeigth}px` }">
|
||||
<div v-for="(item, index) in dataList" :id="item.id" :key="index" :ref="(el) => setItemRefs(el, item)"
|
||||
class="wapper-item" :style="{ width: `${realWidth}px` }">
|
||||
<transition name="img" :css="true">
|
||||
<img :id="item.id" class="item-file rounded-sm"
|
||||
:style="{ width: `${realWidth}px`, height: `${realHeight(item)}px` }" :src="preOrigin ? item.fileInfo.cosUrl : item.fileInfo.thumbImg"
|
||||
loading="lazy" @load="imgLoadSuccess($event, item)" @error="imgLoadError($event, item)"
|
||||
@click="handlePreview(item)" />
|
||||
</transition>
|
||||
<div class="menu p-2 text-[#cbd5e1]">
|
||||
<div class="prompt">
|
||||
{{ item.fullPrompt }}
|
||||
</div>
|
||||
<div class="flex justify-end items-end space-x-2">
|
||||
<n-popover trigger="hover" v-if="isDrawLike" >
|
||||
<template #trigger>
|
||||
<button
|
||||
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
|
||||
@click.stop="drawLike(item)">
|
||||
<span class="text-sm dark:text-slate-400">
|
||||
<SvgIcon icon="fluent:draw-image-24-regular" class="text-sm" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span>画同款</span>
|
||||
</n-popover>
|
||||
|
||||
<n-popover trigger="hover" v-if="usePropmpt" >
|
||||
<template #trigger>
|
||||
<button
|
||||
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
|
||||
@click.stop="usePropmptDraw(item)">
|
||||
<span class="text-sm dark:text-slate-400">
|
||||
<SvgIcon icon="fluent:draw-image-24-regular" class="text-sm" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span>使用当前画同款</span>
|
||||
</n-popover>
|
||||
|
||||
<n-popover trigger="hover" v-if="copyPropmpt">
|
||||
<template #trigger>
|
||||
<button
|
||||
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
|
||||
@click.stop="handleCopy(item)">
|
||||
<span class="text-sm dark:text-slate-400">
|
||||
<SvgIcon icon="tabler:copy" class="text-sm" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span>复制提示词</span>
|
||||
</n-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-loading" v-if="!loadComplete.includes(item.id)"
|
||||
:style="{ width: `${realWidth}px`, height: `${realHeight(item)}px` }"></div>
|
||||
</div>
|
||||
<div id="footer" class="w-full absolute bottom-[350px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.market {
|
||||
}
|
||||
|
||||
.wapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
|
||||
|
||||
&-item {
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transition: all 0.5s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.menu {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 94%;
|
||||
left: 3%;
|
||||
max-height: 70%;
|
||||
height: 100px;
|
||||
transform: translateY(100%);
|
||||
background-color: #090b15;
|
||||
opacity: 0.8;
|
||||
transition: all .1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.prompt {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: all .6s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.item-loading {
|
||||
background: url(../../assets/img-bg.png) no-repeat center center;
|
||||
filter: blur(20px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img-enter-active,
|
||||
.img-leave-active {
|
||||
transition: transform .3s;
|
||||
}
|
||||
|
||||
.img-enter,
|
||||
.img-leave-to {
|
||||
transform: scale(.6);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
20
chat/src/components/common/HoverButton/Button.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang='ts'>
|
||||
interface Emit {
|
||||
(e: 'click'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
function handleClick() {
|
||||
emit('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center justify-center w-10 h-8 transition rounded-md hover:bg-neutral-100 dark:hover:bg-[#414755]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
46
chat/src/components/common/HoverButton/index.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed } from 'vue'
|
||||
import type { PopoverPlacement } from 'naive-ui'
|
||||
import { NTooltip } from 'naive-ui'
|
||||
import Button from './Button.vue'
|
||||
|
||||
interface Props {
|
||||
tooltip?: string
|
||||
placement?: PopoverPlacement
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: 'click'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tooltip: '',
|
||||
placement: 'bottom',
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const showTooltip = computed(() => Boolean(props.tooltip))
|
||||
|
||||
function handleClick() {
|
||||
emit('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showTooltip">
|
||||
<NTooltip :placement="placement" trigger="hover">
|
||||
<template #trigger>
|
||||
<Button @click="handleClick">
|
||||
<slot />
|
||||
</Button>
|
||||
</template>
|
||||
{{ tooltip }}
|
||||
</NTooltip>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Button @click="handleClick">
|
||||
<slot />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
217
chat/src/components/common/ImageEditorCanvas/index.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas ref="canvas" @click="handleClick" crossOrigin="anonymous"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
// 定义接收的属性
|
||||
const props = defineProps({
|
||||
src: String,
|
||||
selectColor: String,
|
||||
maxSteps: Number,
|
||||
updateFileInfo: Function
|
||||
});
|
||||
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
const ctx = ref<CanvasRenderingContext2D | null>(null);
|
||||
let modifiedPixels = new Set<string>();
|
||||
const history = ref<ImageData[]>([]);
|
||||
const maxHistorySteps = ref(10)
|
||||
|
||||
watch(() => props.maxSteps, (val) => {
|
||||
val && (maxHistorySteps.value = val)
|
||||
}, { immediate: true })
|
||||
// 初始化canvas
|
||||
onMounted(() => {
|
||||
if (canvas.value) {
|
||||
ctx.value = canvas.value.getContext('2d', { willReadFrequently: true });
|
||||
initCanvas();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听src属性变化
|
||||
watch(() => props.src, (newSrc) => {
|
||||
if (newSrc) {
|
||||
initCanvas();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化Canvas函数
|
||||
function initCanvas(){
|
||||
if (!ctx.value || !props.src) return;
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
canvas.value!.width = img.width;
|
||||
canvas.value!.height = img.height;
|
||||
props.updateFileInfo?.({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
scaleRatio: 1,
|
||||
})
|
||||
ctx.value!.drawImage(img, 0, 0, img.width, img.height);
|
||||
};
|
||||
img.src = props.src;
|
||||
};
|
||||
|
||||
// 获取下标
|
||||
function pointToIndex (x: number, y: number) {
|
||||
return (y * canvas.value!.width + x) * 4;
|
||||
};
|
||||
|
||||
// 获取颜色
|
||||
function getColor(x: number, y: number, imgData: Uint8ClampedArray) {
|
||||
const i = pointToIndex(x, y);
|
||||
return [
|
||||
imgData[i],
|
||||
imgData[i + 1],
|
||||
imgData[i + 2],
|
||||
imgData[i + 3],
|
||||
];
|
||||
};
|
||||
|
||||
function diff (color1: number[], color2: number[]) {
|
||||
const sum = color1.reduce((sum, value, index) => sum + Math.abs(value - color2[index]), 0);
|
||||
return sum;
|
||||
};
|
||||
|
||||
// 修改颜色
|
||||
function changeColor (initX: number, initY: number, targetColor: number[], clickColor: number[], imgData: Uint8ClampedArray){
|
||||
// 保存当前状态
|
||||
if (ctx.value && canvas.value) {
|
||||
const currentImageData = ctx.value.getImageData(0, 0, canvas.value.width, canvas.value.height);
|
||||
addAction(currentImageData)
|
||||
}
|
||||
const queue = [[initX, initY]];
|
||||
while(queue.length) {
|
||||
const [x, y] = queue.shift()!;
|
||||
if(x < 0 || x >= canvas.value!.width || y < 0 || y >= canvas.value!.height) continue;
|
||||
const curColor = getColor(x, y, imgData);
|
||||
if(diff(curColor, clickColor) > 50) continue;
|
||||
if(diff(curColor, targetColor) === 0) continue;
|
||||
const i = pointToIndex(x, y);
|
||||
imgData.set(targetColor, i);
|
||||
modifiedPixels.add(x + "," + y);
|
||||
queue.push([x+1, y]);
|
||||
queue.push([x-1, y]);
|
||||
queue.push([x, y+1]);
|
||||
queue.push([x, y-1]);
|
||||
}
|
||||
};
|
||||
|
||||
/* 点选图片换色 */
|
||||
function handleClick(e: MouseEvent){
|
||||
if (!ctx.value || !canvas.value) return;
|
||||
const x = e.offsetX;
|
||||
const y = e.offsetY;
|
||||
const imageData = ctx.value.getImageData(0, 0, canvas.value.width, canvas.value.height);
|
||||
const clickColor = getColor(x, y, imageData.data);
|
||||
const color = parseColor(props.selectColor);
|
||||
changeColor(x, y, color, clickColor, imageData.data);
|
||||
ctx.value.putImageData(imageData, 0, 0);
|
||||
};
|
||||
|
||||
/* 获得base64 */
|
||||
function exportToBase64WithCustomBackground() {
|
||||
if (!ctx.value || !canvas.value) return '';
|
||||
const originalImageData = ctx.value.getImageData(0, 0, canvas.value.width, canvas.value.height);
|
||||
const copiedData = new Uint8ClampedArray(originalImageData.data);
|
||||
for (let y = 0; y < canvas.value.height; y++) {
|
||||
for (let x = 0; x < canvas.value.width; x++) {
|
||||
const i = pointToIndex(x, y);
|
||||
if (modifiedPixels.has(x + "," + y)) {
|
||||
copiedData[i] = 255;
|
||||
copiedData[i + 1] = 255;
|
||||
copiedData[i + 2] = 255;
|
||||
} else {
|
||||
copiedData[i] = 0;
|
||||
copiedData[i + 1] = 0;
|
||||
copiedData[i + 2] = 0;
|
||||
}
|
||||
copiedData[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
const newImageData = new ImageData(copiedData, canvas.value.width, canvas.value.height);
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
tempCanvas.width = canvas.value.width;
|
||||
tempCanvas.height = canvas.value.height;
|
||||
const tempCtx = tempCanvas.getContext('2d')!;
|
||||
tempCtx.putImageData(newImageData, 0, 0);
|
||||
return tempCanvas.toDataURL("image/png");
|
||||
};
|
||||
/* 格式化传入颜色 */
|
||||
function parseColor(selectColor: string): number[]{
|
||||
if (selectColor && selectColor.startsWith('#')) {
|
||||
const extendedHex = selectColor.length === 4 ? '#' + selectColor[1] + selectColor[1] + selectColor[2] + selectColor[2] + selectColor[3] + selectColor[3] : selectColor;
|
||||
const r = parseInt(extendedHex.slice(1, 3), 16);
|
||||
const g = parseInt(extendedHex.slice(3, 5), 16);
|
||||
const b = parseInt(extendedHex.slice(5, 7), 16);
|
||||
return [r, g, b, 255];
|
||||
}
|
||||
else if (selectColor && selectColor.startsWith('rgb')) {
|
||||
const rgbValues = selectColor
|
||||
.replace(/rgba?\(/, '')
|
||||
.replace(/\)/, '')
|
||||
.split(',')
|
||||
.map((num) => parseInt(num));
|
||||
if (rgbValues.length === 3) rgbValues.push(255); // 如果没有 alpha,添加一个默认的不透明度
|
||||
return rgbValues;
|
||||
}
|
||||
return [0, 0, 0, 255];
|
||||
};
|
||||
/* 对外提供base64格式的遮罩 */
|
||||
async function getBase(){
|
||||
return await exportToBase64WithCustomBackground()
|
||||
}
|
||||
/* 返回上一步 */
|
||||
function undo() {
|
||||
if(history.value.length === 0 || !ctx.value || !canvas.value) return;
|
||||
const previousState = history.value.pop();
|
||||
ctx.value.putImageData(previousState.imageData, 0, 0);
|
||||
modifiedPixels = new Set(previousState.currentModifiedPixels);
|
||||
}
|
||||
/* 清空画布 */
|
||||
function clear() {
|
||||
if (!ctx.value || !canvas.value) return;
|
||||
|
||||
// 直接清空画布
|
||||
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||
|
||||
// 重置修改记录和历史记录
|
||||
modifiedPixels.clear();
|
||||
history.value = []; // 如果您想保留初始状态,可以重置为 [initialState]
|
||||
|
||||
initCanvas();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* 设置记录栈最大存储步数 防止存储过多 */
|
||||
function setMaxHistorySteps(steps) {
|
||||
maxHistorySteps.value = steps;
|
||||
}
|
||||
|
||||
/* 检测、超出限制移除老的数据 */
|
||||
function addAction(imageData) {
|
||||
const currentModifiedPixels = new Set(modifiedPixels); // 创建 modifiedPixels 的一个副本
|
||||
history.value.push({ imageData, currentModifiedPixels }); // 保存 imageData 和 modifiedPixels
|
||||
if (history.value.length > maxHistorySteps.value) {
|
||||
history.value.shift();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getBase,
|
||||
undo,
|
||||
clear
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 这里可以添加样式 */
|
||||
</style>
|
||||
43
chat/src/components/common/NaiveProvider/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, h } from 'vue'
|
||||
import {
|
||||
NDialogProvider,
|
||||
NLoadingBarProvider,
|
||||
NMessageProvider,
|
||||
NNotificationProvider,
|
||||
useDialog,
|
||||
useLoadingBar,
|
||||
useMessage,
|
||||
useNotification,
|
||||
} from 'naive-ui'
|
||||
|
||||
function registerNaiveTools() {
|
||||
window.$loadingBar = useLoadingBar()
|
||||
window.$dialog = useDialog()
|
||||
window.$message = useMessage()
|
||||
window.$notification = useNotification()
|
||||
}
|
||||
|
||||
const NaiveProviderContent = defineComponent({
|
||||
name: 'NaiveProviderContent',
|
||||
setup() {
|
||||
registerNaiveTools()
|
||||
},
|
||||
render() {
|
||||
return h('div')
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLoadingBarProvider>
|
||||
<NDialogProvider>
|
||||
<NNotificationProvider>
|
||||
<NMessageProvider>
|
||||
<slot />
|
||||
<NaiveProviderContent />
|
||||
</NMessageProvider>
|
||||
</NNotificationProvider>
|
||||
</NDialogProvider>
|
||||
</NLoadingBarProvider>
|
||||
</template>
|
||||
73
chat/src/components/common/QRCode/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { useQRCode } from '@vueuse/integrations/useQRCode'
|
||||
/*
|
||||
参考文档:https://vueuse.org/integrations/useQRCode/
|
||||
https://www.npmjs.com/package/qrcode#qr-code-options
|
||||
*/
|
||||
interface Props {
|
||||
value?: string // 扫描后的文本或地址
|
||||
size?: number // 二维码大小
|
||||
color?: string // 二维码颜色,Value must be in hex format (十六进制颜色值)
|
||||
backgroundColor?: string // 二维码背景色,Value must be in hex format (十六进制颜色值)
|
||||
bordered?: boolean // 是否有边框
|
||||
borderColor?: string // 边框颜色
|
||||
scale?: number // 每个black dots多少像素
|
||||
/*
|
||||
纠错等级也叫纠错率,就是指二维码可以被遮挡后还能正常扫描,而这个能被遮挡的最大面积就是纠错率。
|
||||
通常情况下二维码分为 4 个纠错级别:L级 可纠正约 7% 错误、M级 可纠正约 15% 错误、Q级 可纠正约 25% 错误、H级 可纠正约30% 错误。
|
||||
并不是所有位置都可以缺损,像最明显的三个角上的方框,直接影响初始定位。中间零散的部分是内容编码,可以容忍缺损。
|
||||
当二维码的内容编码携带信息比较少的时候,也就是链接比较短的时候,设置不同的纠错等级,生成的图片不会发生变化。
|
||||
*/
|
||||
errorLevel?: string // 二维码纠错等级
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: '',
|
||||
size: 160,
|
||||
color: '#000',
|
||||
backgroundColor: '#FFF',
|
||||
bordered: true,
|
||||
borderColor: '#0505050f',
|
||||
scale: 8,
|
||||
errorLevel: 'H', // 可选 L M Q H
|
||||
})
|
||||
|
||||
// `qrcode` will be a ref of data URL
|
||||
const qrcode = useQRCode(props.value, {
|
||||
errorCorrectionLevel: props.errorLevel,
|
||||
type: 'image/png',
|
||||
quality: 1,
|
||||
margin: 3,
|
||||
scale: props.scale, // 8px per modules(black dots)
|
||||
color: {
|
||||
dark: props.color, // 像素点颜色
|
||||
light: props.backgroundColor, // 背景色
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-qrcode" :class="{ bordered }" :style="`width: ${size}px; height: ${size}px; border-color: ${borderColor};`">
|
||||
<img :src="qrcode" class="u-qrcode" alt="QRCode">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.m-qrcode {
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.u-qrcode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.bordered {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
</style>
|
||||
46
chat/src/components/common/Setting/Advanced.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { NButton, NInput, useMessage } from 'naive-ui'
|
||||
import { useSettingStore } from '@/store'
|
||||
import type { SettingsState } from '@/store/modules/settings/helper'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const ms = useMessage()
|
||||
|
||||
const systemMessage = ref(settingStore.systemMessage ?? '')
|
||||
|
||||
function updateSettings(options: Partial<SettingsState>) {
|
||||
settingStore.updateSetting(options)
|
||||
ms.success(t('common.success'))
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
settingStore.resetSetting()
|
||||
ms.success(t('common.success'))
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 space-y-5 min-h-[200px]">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.role') }}</span>
|
||||
<div class="flex-1">
|
||||
<NInput v-model:value="systemMessage" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]"> </span>
|
||||
<NButton size="small" @click="handleReset">
|
||||
{{ $t('common.reset') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
213
chat/src/components/common/Setting/General.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
|
||||
import type { Language, Theme } from '@/store/modules/app/helper'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useAppStore, useAuthStore } from '@/store'
|
||||
import { getCurrentDate } from '@/utils/functions'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
import { fetchUpdateInfoAPI } from '@/api/index'
|
||||
import type { ResData } from '@/api/types'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const ms = useMessage()
|
||||
|
||||
const theme = computed(() => appStore.theme)
|
||||
|
||||
const userInfo = computed(() => authStore.userInfo)
|
||||
|
||||
const avatar = ref(userInfo.value.avatar ?? '')
|
||||
|
||||
const username = ref(userInfo.value.username ?? '')
|
||||
|
||||
const btnDisabled = ref(false)
|
||||
|
||||
const language = computed({
|
||||
get() {
|
||||
return appStore.language
|
||||
},
|
||||
set(value: Language) {
|
||||
appStore.setLanguage(value)
|
||||
},
|
||||
})
|
||||
|
||||
const themeOptions: { label: string; key: Theme; icon: string }[] = [
|
||||
{
|
||||
label: 'Auto',
|
||||
key: 'auto',
|
||||
icon: 'ri:contrast-line',
|
||||
},
|
||||
{
|
||||
label: 'Light',
|
||||
key: 'light',
|
||||
icon: 'ri:sun-foggy-line',
|
||||
},
|
||||
{
|
||||
label: 'Dark',
|
||||
key: 'dark',
|
||||
icon: 'ri:moon-foggy-line',
|
||||
},
|
||||
]
|
||||
|
||||
const languageOptions: { label: string; key: Language; value: Language }[] = [
|
||||
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
|
||||
// { label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
|
||||
// { label: 'English', key: 'en-US', value: 'en-US' },
|
||||
]
|
||||
|
||||
async function updateUserInfo(options: { avatar?: string; username?: string }) {
|
||||
try {
|
||||
btnDisabled.value = true
|
||||
const res: ResData = await fetchUpdateInfoAPI(options)
|
||||
btnDisabled.value = false
|
||||
if (!res.success)
|
||||
return ms.error(res.message)
|
||||
ms.success(t('common.updateUserSuccess'))
|
||||
authStore.getUserInfo()
|
||||
}
|
||||
catch (error) {
|
||||
btnDisabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function exportData(): void {
|
||||
const date = getCurrentDate()
|
||||
const data: string = localStorage.getItem('chatStorage') || '{}'
|
||||
const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
|
||||
const blob: Blob = new Blob([jsonString], { type: 'application/json' })
|
||||
const url: string = URL.createObjectURL(blob)
|
||||
const link: HTMLAnchorElement = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `chat-store_${date}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
function importData(event: Event): void {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (!target || !target.files)
|
||||
return
|
||||
|
||||
const file: File = target.files[0]
|
||||
if (!file)
|
||||
return
|
||||
|
||||
const reader: FileReader = new FileReader()
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const data = JSON.parse(reader.result as string)
|
||||
localStorage.setItem('chatStorage', JSON.stringify(data))
|
||||
ms.success(t('common.success'))
|
||||
location.reload()
|
||||
}
|
||||
catch (error) {
|
||||
ms.error(t('common.invalidFileFormat'))
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
function clearData(): void {
|
||||
localStorage.removeItem('chatStorage')
|
||||
location.reload()
|
||||
}
|
||||
|
||||
function handleImportButtonClick(): void {
|
||||
const fileInput = document.getElementById('fileInput') as HTMLElement
|
||||
if (fileInput)
|
||||
fileInput.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 space-y-5 min-h-[200px]">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
|
||||
<div class="flex-1">
|
||||
<NInput v-model:value="avatar" placeholder="请填写头像地址" />
|
||||
</div>
|
||||
<NButton size="tiny" :disabled="btnDisabled" text type="primary" @click="updateUserInfo({ avatar })">
|
||||
{{ $t('common.update') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
|
||||
<div class="w-[200px]">
|
||||
<NInput v-model:value="username" placeholder="请填写用户名" />
|
||||
</div>
|
||||
<NButton size="tiny" :disabled="btnDisabled" text type="primary" @click="updateUserInfo({ username })">
|
||||
{{ $t('common.update') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center space-x-4"
|
||||
:class="isMobile && 'items-start'"
|
||||
>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<NButton size="small" @click="exportData">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:download-2-fill" />
|
||||
</template>
|
||||
{{ $t('common.export') }}
|
||||
</NButton>
|
||||
|
||||
<input id="fileInput" type="file" style="display:none" @change="importData">
|
||||
<NButton size="small" @click="handleImportButtonClick">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:upload-2-fill" />
|
||||
</template>
|
||||
{{ $t('common.import') }}
|
||||
</NButton>
|
||||
|
||||
<NPopconfirm placement="bottom" @positive-click="clearData">
|
||||
<template #trigger>
|
||||
<NButton size="small">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:close-circle-line" />
|
||||
</template>
|
||||
{{ $t('common.clear') }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{ $t('chat.clearHistoryConfirm') }}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<template v-for="item of themeOptions" :key="item.key">
|
||||
<NButton
|
||||
size="small"
|
||||
:type="item.key === theme ? 'primary' : undefined"
|
||||
@click="appStore.setTheme(item.key)"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon :icon="item.icon" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<NSelect
|
||||
style="width: 140px"
|
||||
:value="language"
|
||||
:options="languageOptions"
|
||||
@update-value="value => appStore.setLanguage(value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
66
chat/src/components/common/Setting/Personal.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang='ts'>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { NSpin } from 'naive-ui'
|
||||
import { useAuthStore } from '@/store'
|
||||
const authStore = useAuthStore()
|
||||
const { userInfo, userBalance } = authStore
|
||||
|
||||
const loading = ref(false)
|
||||
onMounted(async () => {
|
||||
getInfo()
|
||||
})
|
||||
|
||||
async function getInfo() {
|
||||
try {
|
||||
loading.value = true
|
||||
await authStore.getUserInfo()
|
||||
loading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpin :show="loading">
|
||||
<div class="p-4 space-y-5 min-h-[200px]">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">用户邮箱</span>
|
||||
<div class="w-[200px]">
|
||||
{{ userInfo.email || "--" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">用户姓名</span>
|
||||
<div class="w-[200px]">
|
||||
{{ userInfo.username || "--" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">问答余额</span>
|
||||
<div class="w-[200px]">
|
||||
{{ userBalance.usesLeft || "0" }} 积分
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">绘画余额</span>
|
||||
<div class="w-[200px]">
|
||||
{{ userBalance.paintCount || "0" }} 积分
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">MJToken</span>
|
||||
<div class="w-[200px]">
|
||||
{{ userBalance.balance || "0" }} Token
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">使用金额</span>
|
||||
<div class="w-[200px]">
|
||||
{{ userBalance.useTokens || "0" }} Token
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NSpin>
|
||||
</template>
|
||||
64
chat/src/components/common/Setting/index.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed, ref } from 'vue'
|
||||
import { NModal, NTabPane, NTabs } from 'naive-ui'
|
||||
import General from './General.vue'
|
||||
import Personal from './Personal.vue'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const active = ref('personalInfo')
|
||||
|
||||
const show = computed({
|
||||
get() {
|
||||
return props.visible
|
||||
},
|
||||
set(visible: boolean) {
|
||||
emit('update:visible', visible)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" title="个人中心" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
|
||||
<div>
|
||||
<NTabs v-model:value="active" type="line" animated>
|
||||
<NTabPane name="personalInfo" tab="personalInfo">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:file-user-line" />
|
||||
<span class="ml-2">{{ $t('setting.personalInfo') }}</span>
|
||||
</template>
|
||||
<Personal />
|
||||
</NTabPane>
|
||||
<NTabPane name="General" tab="General">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
|
||||
<span class="ml-2">{{ $t('setting.general') }}</span>
|
||||
</template>
|
||||
<div class="min-h-[100px]">
|
||||
<General />
|
||||
</div>
|
||||
</NTabPane>
|
||||
<!-- <NTabPane name="Advanced" tab="Advanced">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
|
||||
<span class="ml-2">{{ $t('setting.advanced') }}</span>
|
||||
</template>
|
||||
<div class="min-h-[100px]">
|
||||
<Advanced />
|
||||
</div>
|
||||
</NTabPane> -->
|
||||
</NTabs>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
21
chat/src/components/common/SvgIcon/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed, useAttrs } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
interface Props {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
||||
class: (attrs.class as string) || '',
|
||||
style: (attrs.style as string) || 'width: 1em, height: 1em',
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon :icon="icon" v-bind="bindAttrs" />
|
||||
</template>
|
||||
52
chat/src/components/common/UserAvatar/index.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed, defineAsyncComponent, ref } from 'vue'
|
||||
import { NAvatar, NButton } from 'naive-ui'
|
||||
import { useAuthStore } from '@/store'
|
||||
import defaultAvatar from '@/assets/avatar.png'
|
||||
import { isString } from '@/utils/is'
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))
|
||||
const userInfo = computed(() => authStore.userInfo)
|
||||
const loginComplete = computed(() => authStore.token)
|
||||
const show = ref(false)
|
||||
|
||||
function openDialog() {
|
||||
if (!loginComplete.value)
|
||||
authStore.setLoginDialog(true)
|
||||
else
|
||||
show.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center overflow-hidden">
|
||||
<div class="w-10 h-10 overflow-hidden rounded-full shrink-0">
|
||||
<template v-if="isString(userInfo.avatar) && userInfo.avatar.length > 0">
|
||||
<NAvatar
|
||||
class="cursor-pointer"
|
||||
size="large"
|
||||
round
|
||||
:src="userInfo.avatar"
|
||||
:fallback-src="defaultAvatar"
|
||||
@click="openDialog"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NAvatar size="large" round :src="defaultAvatar" @click="openDialog" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 ml-2">
|
||||
<h2 v-if="loginComplete" class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap cursor-pointer" @click="show = true">
|
||||
{{ userInfo.username ?? '未登录' }}
|
||||
</h2>
|
||||
<NButton v-if="!loginComplete" text @click="authStore.setLoginDialog(true)">
|
||||
登录/注册
|
||||
</NButton>
|
||||
<!-- <p class="overflow-hidden text-xs text-gray-500 text-ellipsis whitespace-nowrap">
|
||||
<span> 点击购买卡密</span>
|
||||
</p> -->
|
||||
</div>
|
||||
<Setting v-if="show" v-model:visible="show" />
|
||||
</div>
|
||||
</template>
|
||||
7
chat/src/components/common/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import HoverButton from './HoverButton/index.vue'
|
||||
import NaiveProvider from './NaiveProvider/index.vue'
|
||||
import SvgIcon from './SvgIcon/index.vue'
|
||||
import UserAvatar from './UserAvatar/index.vue'
|
||||
import Setting from './Setting/index.vue'
|
||||
|
||||
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting }
|
||||
22
chat/src/constants/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
interface RechargeType {
|
||||
[key: number]: string
|
||||
}
|
||||
|
||||
export const RechargeTypeMap: RechargeType = {
|
||||
1: '注册赠送',
|
||||
2: '受邀请赠送',
|
||||
3: '邀请他人赠送',
|
||||
4: '购买卡密充值',
|
||||
5: '管理员赠送',
|
||||
6: '扫码购买充值',
|
||||
7: 'MJ绘画失败退款',
|
||||
8: '签到奖励',
|
||||
}
|
||||
|
||||
// 0:未支付、1:已支付、2、支付失败、3:支付超时)
|
||||
export const OrderMap = {
|
||||
0: '未支付',
|
||||
1: '已支付',
|
||||
2: '支付失败',
|
||||
3: '支付超时',
|
||||
}
|
||||
10
chat/src/hooks/useBasicLayout.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
export function useBasicLayout() {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMobile = breakpoints.smaller('sm')
|
||||
const isSmallMd = breakpoints.smaller('md')
|
||||
const isSmallLg = breakpoints.smaller('lg')
|
||||
const isSmallXl = breakpoints.smaller('xl')
|
||||
|
||||
return { isMobile, isSmallMd, isSmallLg, isSmallXl }
|
||||
}
|
||||
36
chat/src/hooks/useIconRender.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { h } from 'vue'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
|
||||
export const useIconRender = () => {
|
||||
interface IconConfig {
|
||||
icon?: string
|
||||
color?: string
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
interface IconStyle {
|
||||
color?: string
|
||||
fontSize?: string
|
||||
}
|
||||
|
||||
const iconRender = (config: IconConfig) => {
|
||||
const { color, fontSize, icon } = config
|
||||
|
||||
const style: IconStyle = {}
|
||||
|
||||
if (color)
|
||||
style.color = color
|
||||
|
||||
if (fontSize)
|
||||
style.fontSize = `${fontSize}px`
|
||||
|
||||
if (!icon)
|
||||
window.console.warn('iconRender: icon is required')
|
||||
|
||||
return () => h(SvgIcon, { icon, style })
|
||||
}
|
||||
|
||||
return {
|
||||
iconRender,
|
||||
}
|
||||
}
|
||||
27
chat/src/hooks/useLanguage.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { computed } from 'vue'
|
||||
import { enUS, zhCN, zhTW } from 'naive-ui'
|
||||
import { useAppStore } from '@/store'
|
||||
import { setLocale } from '@/locales'
|
||||
|
||||
export function useLanguage() {
|
||||
const appStore = useAppStore()
|
||||
|
||||
const language = computed(() => {
|
||||
switch (appStore.language) {
|
||||
case 'en-US':
|
||||
setLocale('en-US')
|
||||
return enUS
|
||||
case 'zh-CN':
|
||||
setLocale('zh-CN')
|
||||
return zhCN
|
||||
case 'zh-TW':
|
||||
setLocale('zh-TW')
|
||||
return zhTW
|
||||
default:
|
||||
setLocale('zh-CN')
|
||||
return enUS
|
||||
}
|
||||
})
|
||||
|
||||
return { language }
|
||||
}
|
||||
79
chat/src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { GlobalThemeOverrides } from 'naive-ui'
|
||||
import { computed, watch } from 'vue'
|
||||
import { darkTheme, useOsTheme } from 'naive-ui'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
export function useTheme() {
|
||||
const appStore = useAppStore()
|
||||
|
||||
const OsTheme = useOsTheme()
|
||||
|
||||
const isDark = computed(() => {
|
||||
if (appStore.theme === 'auto')
|
||||
return OsTheme.value === 'dark'
|
||||
else
|
||||
return appStore.theme === 'dark'
|
||||
})
|
||||
|
||||
const theme = computed(() => {
|
||||
return isDark.value ? darkTheme : undefined
|
||||
})
|
||||
|
||||
const themeOverrides = computed<GlobalThemeOverrides>(() => {
|
||||
if (isDark.value) {
|
||||
return {
|
||||
common: {},
|
||||
}
|
||||
}
|
||||
return {
|
||||
common: {
|
||||
primaryColor: '#409eff',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const darkThemeOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#5A91FCFF',
|
||||
primaryColorHover: '#3074F8FF',
|
||||
primaryColorPressed: '#3671E4FF',
|
||||
baseColor: '#ffffff',
|
||||
},
|
||||
Switch: {
|
||||
railColorActive: '#5A91FCFF',
|
||||
},
|
||||
Layout: {
|
||||
// color: '#101014FF',
|
||||
// siderColor: '#2F2E34',
|
||||
},
|
||||
}
|
||||
|
||||
const lightThemeOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#5A91FCFF',
|
||||
primaryColorHover: '#3074F8FF',
|
||||
primaryColorPressed: '#3671E4FF'
|
||||
},
|
||||
Skeleton: {
|
||||
color: '#F4F3F3FF',
|
||||
colorEnd: '#F1F0F0FF'
|
||||
},
|
||||
Layout: {
|
||||
// color: '#F7F9FAFF',
|
||||
// siderColor: '#EAF3F0FF',
|
||||
},
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isDark.value,
|
||||
(dark) => {
|
||||
if (dark)
|
||||
document.documentElement.classList.add('dark')
|
||||
else
|
||||
document.documentElement.classList.remove('dark')
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return { theme, themeOverrides, lightThemeOverrides, darkThemeOverrides }
|
||||
}
|
||||
5
chat/src/icons/403.vue
Normal file
1
chat/src/icons/404.svg
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
5
chat/src/icons/500.vue
Normal file
118
chat/src/layout/components/BindWx.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup lang='ts'>
|
||||
import type { CountdownInst } from 'naive-ui'
|
||||
import { NCountdown, NIcon, NImage, NModal, NSkeleton, NSpin, useMessage } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
import { CloseOutline, PaperPlaneOutline } from '@vicons/ionicons5'
|
||||
import { fetchBindWxBySceneStrAPI, fetchGetQRCodeAPI, fetchGetQRSceneStrByBindAPI } from '@/api/user'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import type { ResData } from '@/api/types'
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
let timer: any
|
||||
const countdownRef = ref<CountdownInst | null>()
|
||||
const authStore = useAuthStore()
|
||||
const activeCount = ref(false)
|
||||
const wxLoginUrl = ref('')
|
||||
const sceneStr = ref('')
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
const Nmessage = useMessage()
|
||||
|
||||
async function getSeneStr() {
|
||||
const res: ResData = await fetchGetQRSceneStrByBindAPI()
|
||||
if (res.success) {
|
||||
sceneStr.value = res.data
|
||||
getQrCodeUrl()
|
||||
}
|
||||
}
|
||||
|
||||
async function getQrCodeUrl() {
|
||||
const res: ResData = await fetchGetQRCodeAPI({ sceneStr: sceneStr.value })
|
||||
if (res.success) {
|
||||
activeCount.value = true
|
||||
wxLoginUrl.value = res.data
|
||||
timer = setInterval(() => {
|
||||
bindWxBySnece()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
async function bindWxBySnece() {
|
||||
if (!sceneStr.value)
|
||||
return
|
||||
const res: ResData = await fetchBindWxBySceneStrAPI({ sceneStr: sceneStr.value })
|
||||
if (res.data) {
|
||||
clearInterval(timer)
|
||||
const { status, msg } = res.data
|
||||
if (status)
|
||||
Nmessage.success(msg)
|
||||
|
||||
else
|
||||
Nmessage.error(msg)
|
||||
|
||||
authStore.getUserInfo()
|
||||
useGlobalStore.updateBindwxDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimeDown() {
|
||||
clearInterval(timer)
|
||||
getSeneStr()
|
||||
countdownRef.value?.reset()
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
getSeneStr()
|
||||
}
|
||||
|
||||
function handleCloseDialog() {
|
||||
clearInterval(timer)
|
||||
wxLoginUrl.value = ''
|
||||
sceneStr.value = ''
|
||||
activeCount.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" style="width: 90%; max-width: 700px" :on-after-enter="openDialog" :on-after-leave="handleCloseDialog">
|
||||
<div class="p-5 bg-white rounded dark:bg-slate-800">
|
||||
<div class="absolute top-3 right-3 cursor-pointer" @click="useGlobalStore.updateBindwxDialog(false)">
|
||||
<NIcon size="20" color="#0e7a0d">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
</div>
|
||||
<div class="flex text-xl font-bold mb-[20px] bg-currentflex items-center ">
|
||||
<NIcon size="25" color="#0e7a0d">
|
||||
<PaperPlaneOutline />
|
||||
</NIcon>
|
||||
<span class="ml-[8px]">绑定微信账户</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="white-space: nowrap" class=" w-full text-center font-bold text-sm py-5">
|
||||
<p>
|
||||
请在 <span class="w-[55px] inline-block text-[red] text-left"><NCountdown ref="countdownRef" :active="activeCount" :duration="120 * 1000" :on-finish="handleTimeDown" /></span> 时间内完成绑定
|
||||
</p>
|
||||
</div>
|
||||
<div class="my-2 flex justify-center relative">
|
||||
<NImage
|
||||
v-if="wxLoginUrl"
|
||||
preview-disabled
|
||||
width="230"
|
||||
:src="wxLoginUrl"
|
||||
/>
|
||||
<NSkeleton v-else height="230px" width="230px" />
|
||||
<NSpin v-if="!wxLoginUrl" size="large" class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2" />
|
||||
</div>
|
||||
|
||||
<span class="flex items-center justify-center text-base py-5">
|
||||
打开微信扫码绑定账户
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
292
chat/src/layout/components/GoodsDialog.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<script setup lang='ts'>
|
||||
import { NButton, NCard, NGrid, NGridItem, NIcon, NModal, NSkeleton, NSpace, useDialog, useMessage } from 'naive-ui'
|
||||
import { CloseOutline } from '@vicons/ionicons5'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { fetchGetPackageAPI } from '@/api/crami'
|
||||
import { fetchOrderBuyAPI } from '@/api/order'
|
||||
import { fetchGetJsapiTicketAPI } from '@/api/user'
|
||||
|
||||
import type { ResData } from '@/api/types'
|
||||
import preferentialIcon from '@/assets/images/preferential.png'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
defineProps<Props>()
|
||||
declare let WeixinJSBridge: any
|
||||
declare let wx: any
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const loading = ref(true)
|
||||
const { isSmallMd } = useBasicLayout()
|
||||
const packageList = ref<Pkg[]>([])
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const dialogLoading = ref(false)
|
||||
|
||||
const isWxEnv = computed(() => {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
return ua.match(/MicroMessenger/i) && ua?.match(/MicroMessenger/i)?.[0] === 'micromessenger'
|
||||
})
|
||||
const payPlatform = computed(() => {
|
||||
const { payHupiStatus, payEpayStatus, payMpayStatus, payWechatStatus } = authStore.globalConfig
|
||||
if (Number(payWechatStatus) === 1)
|
||||
return 'wechat'
|
||||
|
||||
if (Number(payMpayStatus) === 1)
|
||||
return 'mpay'
|
||||
|
||||
if (Number(payHupiStatus) === 1)
|
||||
return 'hupi'
|
||||
|
||||
if (Number(payEpayStatus) === 1)
|
||||
return 'epay'
|
||||
return null
|
||||
})
|
||||
|
||||
const payChannel = computed(() => {
|
||||
const { payEpayChannel, payMpayChannel } = authStore.globalConfig
|
||||
if (payPlatform.value === 'mpay')
|
||||
return payMpayChannel ? JSON.parse(payMpayChannel) : []
|
||||
|
||||
if (payPlatform.value === 'epay')
|
||||
return payEpayChannel ? JSON.parse(payEpayChannel) : []
|
||||
|
||||
if (payPlatform.value === 'wechat')
|
||||
return ['wxpay']
|
||||
|
||||
if (payPlatform.value === 'hupi')
|
||||
return ['wxpay']
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Pkg {
|
||||
id: number
|
||||
name: string
|
||||
coverImg: string
|
||||
des: string
|
||||
price: number
|
||||
model3Count: number
|
||||
model4Count: number
|
||||
drawMjCount: number
|
||||
extraReward: number
|
||||
extraPaintCount: number
|
||||
createdAt: Date
|
||||
}
|
||||
function openDialog() {
|
||||
openDrawerAfter()
|
||||
if (isWxEnv.value)
|
||||
jsapiInitConfig()
|
||||
}
|
||||
|
||||
function handleCloseDialog() {
|
||||
packageList.value = []
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
/* 微信环境jsapi注册 */
|
||||
async function jsapiInitConfig() {
|
||||
const url = window.location.href.replace(/#.*$/, '')
|
||||
const res: ResData = await fetchGetJsapiTicketAPI({ url })
|
||||
const { appId, nonceStr, timestamp, signature } = res.data
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
wx.config({
|
||||
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
|
||||
appId, // 必填,公众号的唯一标识
|
||||
timestamp, // 必填,生成签名的时间戳
|
||||
nonceStr, // 必填,生成签名的随机串
|
||||
signature, // 必填,签名
|
||||
jsApiList: ['chooseWXPay'], // 必填,需要使用的JS接口列表
|
||||
})
|
||||
wx.ready(() => {})
|
||||
wx.error(() => {})
|
||||
}
|
||||
|
||||
function onBridgeReady(data: { appId: string; timeStamp: string; nonceStr: string; package: string; signType: string; paySign: string }) {
|
||||
const { appId, timeStamp, nonceStr, package: pkg, signType, paySign } = data
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', {
|
||||
appId, // 公众号ID,由商户传入
|
||||
timeStamp, // 时间戳,自1970年以来的秒数
|
||||
nonceStr, // 随机串
|
||||
package: pkg,
|
||||
signType, // 微信签名方式:
|
||||
paySign, // 微信签名
|
||||
},
|
||||
(res: any) => {
|
||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
message.success('购买成功、祝您使用愉快!')
|
||||
setTimeout(() => {
|
||||
authStore.getUserInfo()
|
||||
useGlobalStore.updateGoodsDialog(false)
|
||||
}, 500)
|
||||
}
|
||||
else {
|
||||
message.warning('您还没有支付成功哟!')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleBuyGoods(pkg: Pkg) {
|
||||
if (dialogLoading.value)
|
||||
return
|
||||
|
||||
// 如果是微信环境判断有没有开启微信支付,开启了则直接调用jsapi支付即可
|
||||
if (isWxEnv.value && payPlatform.value === 'wechat' && Number(authStore.globalConfig.payWechatStatus) === 1) {
|
||||
if (typeof WeixinJSBridge == 'undefined') {
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)
|
||||
}
|
||||
else if (document.attachEvent) {
|
||||
document.attachEvent('WeixinJSBridgeReady', onBridgeReady)
|
||||
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady)
|
||||
}
|
||||
}
|
||||
else {
|
||||
const res: ResData = await fetchOrderBuyAPI({ goodsId: pkg.id, payType: 'jsapi' })
|
||||
const { success, data } = res
|
||||
success && onBridgeReady(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/* 其他场景打开支付窗口 */
|
||||
useGlobalStore.updateOrderInfo({ pkgInfo: pkg })
|
||||
useGlobalStore.updateGoodsDialog(false)
|
||||
useGlobalStore.updatePayDialog(true)
|
||||
// dialogLoading.value = true
|
||||
// const { id: goodsId, name, des } = pkg
|
||||
// try {
|
||||
// /* 如果在微信环境 则微信官方支付渠道为jsapi支付 */
|
||||
// if (payPlatform.value === 'wechat')
|
||||
// payType = isWxEnv.value ? 'jsapi' : 'native'
|
||||
|
||||
// const res: ResData = await fetchOrderBuyAPI({ goodsId, payType })
|
||||
// dialogLoading.value = false
|
||||
// const { success, data } = res
|
||||
// if (success) {
|
||||
// /* 如果是微信环境并且开启了微信登录则调用jsapi支付 */
|
||||
// if (isWxEnv.value && payPlatform.value === 'wechat') {
|
||||
// if (typeof WeixinJSBridge == 'undefined') {
|
||||
// if (document.addEventListener) {
|
||||
// document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)
|
||||
// }
|
||||
// else if (document.attachEvent) {
|
||||
// document.attachEvent('WeixinJSBridgeReady', onBridgeReady)
|
||||
// document.attachEvent('onWeixinJSBridgeReady', onBridgeReady)
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// onBridgeReady(data)
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
// useGlobalStore.updateOrderInfo({ ...data, pkgInfo: pkg })
|
||||
// useGlobalStore.updateGoodsDialog(false)
|
||||
// const { isRedirect, redirectUrl } = data
|
||||
// if (isRedirect)
|
||||
// window.open(redirectUrl)
|
||||
|
||||
// else
|
||||
// useGlobalStore.updatePayDialog(true)
|
||||
// }
|
||||
// }
|
||||
// catch (error) {
|
||||
// dialogLoading.value = false
|
||||
// }
|
||||
}
|
||||
|
||||
async function openDrawerAfter() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: ResData = await fetchGetPackageAPI({ status: 1, size: 30 })
|
||||
packageList.value = res.data.rows
|
||||
loading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess(pkg: Pkg) {
|
||||
const { name } = pkg
|
||||
dialog.success({
|
||||
title: '订单确认',
|
||||
content: `欢迎选购、确定购买${name}么!`,
|
||||
negativeText: '我再想想',
|
||||
positiveText: '确认购买',
|
||||
onPositiveClick: () => {
|
||||
if (!payChannel.value.length)
|
||||
message.warning('管理员还未开启支付!')
|
||||
|
||||
handleBuyGoods(pkg)
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" :style="{ maxWidth: `${packageList.length > 4 ? 1200 : packageList.length * 250}px`, minWidth: isSmallMd ? '100%' : '1000px' }" :on-after-enter="openDialog" :on-after-leave="handleCloseDialog">
|
||||
<div class="p-4 bg-white rounded dark:bg-slate-800 max-h-4/5">
|
||||
<div class=" flex cursor-pointer justify-between ">
|
||||
<span class="text-xl">选购商品</span>
|
||||
<NIcon size="20" color="#0e7a0d" @click="useGlobalStore.updateGoodsDialog(false)">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
</div>
|
||||
<div v-if="!loading" class="p-4">
|
||||
<NGrid :x-gap="15" :y-gap="15" :cols="isSmallMd ? 1 : packageList.length > 4 ? 4 : packageList.length" class="mt-3">
|
||||
<NGridItem v-for="(item, index) in packageList" :key="index">
|
||||
<NCard size="small" embedded>
|
||||
<template #header>
|
||||
<div class="relative">
|
||||
<b>{{ item.name }}</b>
|
||||
<img v-if="item.extraReward === 1" :src="preferentialIcon" class="w-8 absolute -right-4 -top-3">
|
||||
</div>
|
||||
</template>
|
||||
<template #cover>
|
||||
<img :src="item.coverImg" class="h-[130px] object-cover">
|
||||
</template>
|
||||
<div>
|
||||
<p>{{ item.des }}</p>
|
||||
<div class="flex justify-between items-end min-h-28">
|
||||
<span class="text-sm font-bold mr-1 w-[120px]">基础模型额度</span>
|
||||
<span class="font-bold">{{ item.model3Count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-end min-h-28">
|
||||
<span class="text-sm font-bold mr-1 w-[120px]">高级模型额度</span>
|
||||
<span class="font-bold">{{ item.model4Count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-end min-h-28">
|
||||
<span class="text-sm font-bold mr-1 w-[120px]">MJ绘画额度</span>
|
||||
<span class="font-bold">{{ item.drawMjCount }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-end mt-5">
|
||||
<i class="text-xl text-[red] font-bold">{{ `¥${item.price}` }}</i>
|
||||
<NButton type="primary" dashed size="small" @click="handleSuccess(item)">
|
||||
购买套餐
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
<div v-if="loading" class="p-4">
|
||||
<NGrid :x-gap="15" :y-gap="15" :cols="isSmallMd ? 1 : 4" class="mt-3">
|
||||
<NGridItem v-for="(index) in 4" :key="index">
|
||||
<NSpace vertical>
|
||||
<NSkeleton height="130px" width="100%" />
|
||||
<NSkeleton height="210px" width="100%" :sharp="false" />
|
||||
</NSpace>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
98
chat/src/layout/components/Login copy.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup lang='ts'>
|
||||
import { NAlert, NButton, NIcon, NModal, NResult, NTabPane, NTabs } from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { CloseOutline } from '@vicons/ionicons5'
|
||||
import Phone from './Login/Phone.vue'
|
||||
import Email from './Login/Email.vue'
|
||||
import Wechat from './Login/Wechat.vue'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
defineProps<Props>()
|
||||
let timer: any
|
||||
const authStore = useAuthStore()
|
||||
const activeCount = ref(false)
|
||||
const wxLoginUrl = ref('')
|
||||
const sceneStr = ref('')
|
||||
|
||||
const registerSendStatus = computed(() => {
|
||||
return Number(authStore.globalConfig.registerSendStatus)
|
||||
})
|
||||
|
||||
const registerSendModel3Count = computed(() => {
|
||||
return Number(authStore.globalConfig.registerSendModel3Count)
|
||||
})
|
||||
|
||||
const registerSendModel4Count = computed(() => {
|
||||
return Number(authStore.globalConfig.registerSendModel4Count)
|
||||
})
|
||||
|
||||
const registerSendDrawMjCount = computed(() => {
|
||||
return Number(authStore.globalConfig.registerSendDrawMjCount)
|
||||
})
|
||||
|
||||
const wechatRegisterStatus = computed(() => Number(authStore.globalConfig.wechatRegisterStatus) === 1)
|
||||
const phoneRegisterStatus = computed(() => Number(authStore.globalConfig.phoneRegisterStatus) === 1)
|
||||
const emailRegisterStatus = computed(() => Number(authStore.globalConfig.emailRegisterStatus) === 1)
|
||||
|
||||
const disabledReg = computed(() => {
|
||||
return !wechatRegisterStatus.value && !phoneRegisterStatus.value && !emailRegisterStatus.value
|
||||
})
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const registerTips = computed(() => (`首次认证:赠送${registerSendModel3Count.value}积分基础模型 | ${registerSendModel4Count.value}积分高级模型 | ${registerSendDrawMjCount.value}积分绘画`))
|
||||
|
||||
function openDialog() {
|
||||
|
||||
}
|
||||
|
||||
function handleCloseDialog() {
|
||||
clearInterval(timer)
|
||||
wxLoginUrl.value = ''
|
||||
sceneStr.value = ''
|
||||
activeCount.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" style="width: 90%; max-width: 500px" :on-after-enter="openDialog" :on-after-leave="handleCloseDialog">
|
||||
<div class="p-5 bg-white rounded dark:bg-slate-800">
|
||||
<div class="absolute top-3 right-3 cursor-pointer z-30" @click="authStore.setLoginDialog(false)">
|
||||
<NIcon size="20" color="#0e7a0d">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
</div>
|
||||
<!-- register -->
|
||||
<NTabs v-if="!disabledReg" type="line" animated>
|
||||
<NTabPane v-if="wechatRegisterStatus" name="wechat" tab="微信登录">
|
||||
<Wechat />
|
||||
</NTabPane>
|
||||
<NTabPane v-if="emailRegisterStatus" name="email" tab="邮箱号登录">
|
||||
<Email />
|
||||
</NTabPane>
|
||||
<NTabPane v-if="phoneRegisterStatus" name="phone" tab="手机号登录">
|
||||
<Phone />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
<NAlert v-if="registerSendStatus && !disabledReg" type="success" :show-icon="false" class="mt-5">
|
||||
{{ registerTips }}
|
||||
</NAlert>
|
||||
<div v-if="disabledReg">
|
||||
<NResult
|
||||
size="small"
|
||||
status="403"
|
||||
title="网站已经关闭注册通道"
|
||||
description="请联系管理员开通吧"
|
||||
>
|
||||
<template #footer>
|
||||
<NButton @click="authStore.setLoginDialog(false)">
|
||||
知道了
|
||||
</NButton>
|
||||
</template>
|
||||
</NResult>
|
||||
</div>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
119
chat/src/layout/components/Login.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang='ts'>
|
||||
import { NAlert, NButton, NIcon, NModal, NResult, NTabPane, NTabs, TabsInst } from 'naive-ui'
|
||||
import { computed, ref, nextTick } from 'vue'
|
||||
import { CloseOutline } from '@vicons/ionicons5'
|
||||
import Phone from './Login/Phone.vue'
|
||||
import Email from './Login/Email.vue'
|
||||
import Wechat from './Login/Wechat.vue'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import bannerImg from '@/assets/login-banner.png'
|
||||
|
||||
defineProps<Props>()
|
||||
let timer: any
|
||||
const authStore = useAuthStore()
|
||||
const activeCount = ref(false)
|
||||
const wxLoginUrl = ref('')
|
||||
const sceneStr = ref('')
|
||||
const tabsRef = ref<TabsInst | null>(null)
|
||||
const showWxLogin = ref(true)
|
||||
const tabName = ref('email')
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
|
||||
const emailLoginStatus = computed(() => Number(authStore.globalConfig.emailLoginStatus) === 1)
|
||||
const wechatRegisterStatus = computed(() => Number(authStore.globalConfig.wechatRegisterStatus) === 1)
|
||||
const phoneLoginStatus = computed(() => Number(authStore.globalConfig.phoneLoginStatus) === 1)
|
||||
|
||||
/* 没有打开任何登录 */
|
||||
const disabledReg = computed(() => {
|
||||
return !wechatRegisterStatus.value && !phoneLoginStatus.value && !emailLoginStatus.value
|
||||
})
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
|
||||
function openDialog() {
|
||||
/* 没打开微信的话使用邮箱或者手机号 */
|
||||
if(!wechatRegisterStatus.value){
|
||||
showWxLogin.value = false
|
||||
if(phoneLoginStatus.value){
|
||||
changeLoginType('phone')
|
||||
}
|
||||
if(emailLoginStatus.value){
|
||||
changeLoginType('email')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloseDialog() {
|
||||
clearInterval(timer)
|
||||
wxLoginUrl.value = ''
|
||||
sceneStr.value = ''
|
||||
activeCount.value = false
|
||||
}
|
||||
|
||||
/* 切换登录类型 */
|
||||
function changeLoginType(type: string){
|
||||
if(type === 'wechat'){
|
||||
showWxLogin.value = true
|
||||
}else{
|
||||
showWxLogin.value = false
|
||||
tabName.value = type
|
||||
nextTick(() => {
|
||||
tabsRef.value?.syncBarPosition()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" :on-after-enter="openDialog" :on-after-leave="handleCloseDialog">
|
||||
<div class="w-[1100px] h-[600px] bg-transparent rounded-md overflow-hidden dark:bg-slate-800">
|
||||
<div class="absolute top-3 right-3 cursor-pointer z-30" @click="authStore.setLoginDialog(false)">
|
||||
<NIcon size="20" color="#0e7a0d">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
</div>
|
||||
<div class="bg-transparent m-0 flex">
|
||||
<div class="w-[521px] h-[600px]" :style="{background: `url(${bannerImg})`, backgroundSize: 'cover'}" v-if="!isMobile"></div>
|
||||
<div v-if="disabledReg" class="flex-1 bg-white flex justify-center items-center dark:bg-[#34373c] h-[600px]">
|
||||
<NResult
|
||||
size="small"
|
||||
status="403"
|
||||
title="网站已经关闭注册通道"
|
||||
description="请联系管理员开通吧"
|
||||
>
|
||||
<template #footer>
|
||||
<NButton size="small" @click="authStore.setLoginDialog(false)">
|
||||
知道了
|
||||
</NButton>
|
||||
</template>
|
||||
</NResult>
|
||||
</div>
|
||||
<div v-if="!disabledReg" class="flex-1 bg-white dark:bg-[#34373c] h-[600px]">
|
||||
<Wechat v-if="wechatRegisterStatus && showWxLogin" @changeLoginType="changeLoginType" />
|
||||
<div class="mt-[50px]" >
|
||||
<NTabs v-if="!showWxLogin" ref="tabsRef" v-model:value="tabName" animated justify-content="space-evenly" >
|
||||
<NTabPane v-if="emailLoginStatus" name="email" tab="邮箱号登录">
|
||||
<Email @changeLoginType="changeLoginType" />
|
||||
</NTabPane>
|
||||
<NTabPane v-if="phoneLoginStatus" name="phone" tab="手机号登录">
|
||||
<Phone @changeLoginType="changeLoginType" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- register -->
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
266
chat/src/layout/components/Login/Email.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
import { NButton, NForm, NFormItem, NInput, useMessage } from 'naive-ui'
|
||||
import Send from './send.vue'
|
||||
import { fetchCaptchaImg, fetchLoginAPI, fetchRegisterAPI } from '@/api'
|
||||
import { useAppStore, useAuthStore } from '@/store'
|
||||
import Motion from '@/utils/motion/index'
|
||||
import { ss } from '@/utils/storage'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
|
||||
interface Emit {
|
||||
(ev: 'changeLoginType', val: string): void
|
||||
}
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const Nmessage = useMessage()
|
||||
const isLogin = ref(true)
|
||||
const loading = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const captchaSvg = ref('')
|
||||
const theme = computed(() => appStore.theme)
|
||||
|
||||
/* isVerifyEmail 为0或者 */
|
||||
const isVerifyEmail = computed(() => {
|
||||
const v = authStore.globalConfig.isVerifyEmail ? Number(authStore.globalConfig.isVerifyEmail) : 1
|
||||
return v
|
||||
})
|
||||
|
||||
const registerButtonMsg = computed(() => {
|
||||
return isVerifyEmail.value ? '发送激活账户邮件' : '立即注册'
|
||||
})
|
||||
|
||||
const captchaBgColor = computed(() => {
|
||||
return theme.value === 'dark' ? '#363f4f' : '#fff'
|
||||
})
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const registerForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
invitedBy: '',
|
||||
captchaCode: '',
|
||||
captchaId: null,
|
||||
})
|
||||
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
invitedBy: '',
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 30, message: '用户名长度应为 2 到 30 个字符', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 30, message: '密码长度应为 6 到 30 个字符', trigger: 'blur' },
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] },
|
||||
],
|
||||
captchaCode: [
|
||||
{ required: true, message: '请填写验证码', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
const logTips = computed(() => (isLogin.value ? '还没账号? 去注册!' : '已有账号, 去登录!'))
|
||||
|
||||
const wechatRegisterStatus = computed(() => Number(authStore.globalConfig.wechatRegisterStatus) === 1)
|
||||
const phoneLoginStatus = computed(() => Number(authStore.globalConfig.phoneLoginStatus) === 1)
|
||||
const emailRegisterStatus = computed(() => Number(authStore.globalConfig.emailRegisterStatus) === 1)
|
||||
|
||||
function handlerSubmit() {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
loading.value = true
|
||||
const Interface = isLogin.value ? fetchLoginAPI : fetchRegisterAPI
|
||||
const params: any = !isLogin.value ? registerForm.value : { username: loginForm.value.username, password: loginForm.value.password }
|
||||
const res: any = await Interface(params)
|
||||
loading.value = false
|
||||
getCaptchaImg()
|
||||
const { success, message } = res
|
||||
if (!success)
|
||||
return Nmessage.error(message)
|
||||
if (!isLogin.value) {
|
||||
const msg = Number(isVerifyEmail) ? '您的账号激活邮件已经发送,请前往邮箱激活您的账户!' : '您的账号已成功注册、请登录使用吧!'
|
||||
Nmessage.success(msg)
|
||||
const { email, password } = registerForm.value
|
||||
loginForm.value.username = email
|
||||
loginForm.value.password = password
|
||||
isLogin.value = !isLogin.value
|
||||
/* 如果不校验 自动登录 */
|
||||
if (!isVerifyEmail.value)
|
||||
autoLogin()
|
||||
}
|
||||
else {
|
||||
Nmessage.success('账户登录成功、开始体验吧!')
|
||||
authStore.setToken(res.data)
|
||||
authStore.getUserInfo()
|
||||
authStore.setLoginDialog(false)
|
||||
if(isMobile.value){
|
||||
window.location.reload()
|
||||
}
|
||||
ss.remove('invitedBy')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
getCaptchaImg()
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function autoLogin() {
|
||||
const params = { username: loginForm.value.username, password: loginForm.value.password }
|
||||
const res: any = await fetchLoginAPI(params)
|
||||
const { success, message } = res
|
||||
if (!success)
|
||||
return Nmessage.error(message)
|
||||
Nmessage.success('账户登录成功、开始体验吧!')
|
||||
authStore.setToken(res.data)
|
||||
authStore.getUserInfo()
|
||||
authStore.setLoginDialog(false)
|
||||
ss.remove('invitedBy')
|
||||
}
|
||||
|
||||
async function getCaptchaImg() {
|
||||
const res: any = await fetchCaptchaImg({ color: captchaBgColor.value })
|
||||
captchaSvg.value = res.data.svgCode
|
||||
registerForm.value.captchaId = res.data.code
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const code = ss.get('invitedBy')
|
||||
code && (registerForm.value.invitedBy = code)
|
||||
getCaptchaImg()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-[65px]" :class="isLogin ? 'pt-[40px]' : 'pt-5'">
|
||||
<NForm
|
||||
v-if="!isLogin"
|
||||
ref="formRef"
|
||||
:model="registerForm"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
:style="{ maxWidth: '640px' }"
|
||||
>
|
||||
<Motion :delay="50">
|
||||
<NFormItem path="username">
|
||||
<NInput v-model:value="registerForm.username" placeholder="请输入您的用户名昵称" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="120">
|
||||
<NFormItem path="password">
|
||||
<NInput v-model:value="registerForm.password" placeholder="请输入您的账户密码" type="password" :maxlength="30" show-password-on="click" tabindex="0" @keyup.enter="handlerSubmit" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="190">
|
||||
<NFormItem path="email">
|
||||
<NInput v-model:value="registerForm.email" placeholder="请填写您的邮箱账号" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="260">
|
||||
<NFormItem path="captchaCode">
|
||||
<div class="flex items-center w-full space-x-4">
|
||||
<NInput v-model:value="registerForm.captchaCode" class="flex-1" placeholder="请填写图中验证码结果" />
|
||||
<div v-if="captchaSvg">
|
||||
<span class="cursor-pointer rounded" @click="getCaptchaImg" v-html="captchaSvg" />
|
||||
</div>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
|
||||
<Motion :delay="330">
|
||||
<NFormItem path="invitedBy">
|
||||
<NInput v-model:value="registerForm.invitedBy" placeholder="邀请码[非必填]" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
|
||||
<NFormItem>
|
||||
<NButton
|
||||
block
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@click="handlerSubmit"
|
||||
>
|
||||
{{ registerButtonMsg }}
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<!-- login -->
|
||||
<NForm
|
||||
v-if="isLogin"
|
||||
ref="formRef"
|
||||
size="large"
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
:style="{
|
||||
maxWidth: '640px',
|
||||
}"
|
||||
>
|
||||
<Motion :delay="50">
|
||||
<NFormItem path="username">
|
||||
<NInput v-model:value="loginForm.username" placeholder="请输入用户名/邮箱号" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
|
||||
<Motion :delay="120">
|
||||
<NFormItem path="password">
|
||||
<NInput v-model:value="loginForm.password" placeholder="请输入您的账户密码" type="password" :maxlength="30" show-password-on="click" tabindex="0" @keyup.enter="handlerSubmit" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<NFormItem>
|
||||
<NButton
|
||||
block
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
class="!mt-[50px]"
|
||||
@click="handlerSubmit"
|
||||
>
|
||||
登录账户
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</div>
|
||||
<span v-if="emailRegisterStatus" class="flex justify-center cursor-pointer">
|
||||
<NButton text @click="isLogin = !isLogin">{{ logTips }}</NButton>
|
||||
</span>
|
||||
<div class="flex items-center justify-center space-x-5" :class="emailRegisterStatus ? 'mt-[16px]' : 'mt-[36px]'">
|
||||
<NButton v-if="wechatRegisterStatus" ghost class="!px-10" @click="emit('changeLoginType', 'wechat')">
|
||||
<SvgIcon class="text-xl mr-2 text-[#3076fd]" icon="ph:wechat-logo" />
|
||||
微信登录
|
||||
</NButton>
|
||||
<NButton v-if="phoneLoginStatus" ghost class="!px-10" @click="emit('changeLoginType', 'phone')">
|
||||
<SvgIcon class="text-xl mr-2 text-[#3076fd]" icon="clarity:mobile-phone-solid" />
|
||||
手机号登录
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<Motion :delay="800">
|
||||
<div class="px-8">
|
||||
<Send v-if="isLogin" />
|
||||
</div>
|
||||
</Motion>
|
||||
</template>
|
||||
323
chat/src/layout/components/Login/Phone.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { FormInst, FormItemRule, FormRules } from 'naive-ui'
|
||||
import { NButton, NForm, NFormItem, NInput, useMessage } from 'naive-ui'
|
||||
import Send from './send.vue'
|
||||
import { fetchCaptchaImg, fetchLoginByPhoneAPI, fetchRegisterByPhoneAPI, fetchSendSms } from '@/api'
|
||||
import { useAppStore, useAuthStore } from '@/store'
|
||||
import { ss } from '@/utils/storage'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import Motion from '@/utils/motion/index'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
|
||||
interface Emit {
|
||||
(ev: 'changeLoginType', val: string): void
|
||||
}
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const Nmessage = useMessage()
|
||||
const isLogin = ref(true)
|
||||
const loading = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const captchaSvg = ref('')
|
||||
const theme = computed(() => appStore.theme)
|
||||
const isSendCaptcha = ref(false)
|
||||
const lastSendPhoneCodeTime = ref(0)
|
||||
|
||||
const captchaBgColor = computed(() => {
|
||||
return theme.value === 'dark' ? '#363f4f' : '#fff'
|
||||
})
|
||||
|
||||
const registerForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
phoneCode: '',
|
||||
invitedBy: '',
|
||||
captchaCode: '',
|
||||
captchaId: null,
|
||||
})
|
||||
|
||||
const loginForm = ref({
|
||||
password: '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 30, message: '用户名长度应为 2 到 30 个字符', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 30, message: '密码长度应为 6 到 30 个字符', trigger: 'blur' },
|
||||
],
|
||||
phone: [
|
||||
{
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value)
|
||||
return new Error('请输入手机号')
|
||||
|
||||
else if (!/^1[3,4,5,6,7,8,9][0-9]{9}$/.test(value))
|
||||
return new Error('请输入正确格式的手机号')
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
captchaCode: [
|
||||
{ required: true, message: '请填写图形验证码结果', trigger: 'blur' },
|
||||
],
|
||||
phoneCode: [
|
||||
{ required: true, message: '请填写手机验证码', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
const logTips = computed(() => (isLogin.value ? '还没账号?去注册!' : '已有账号, 去登录!'))
|
||||
const wechatRegisterStatus = computed(() => Number(authStore.globalConfig.wechatRegisterStatus) === 1)
|
||||
const phoneRegisterStatus = computed(() => Number(authStore.globalConfig.phoneRegisterStatus) === 1)
|
||||
const phoneLoginStatus = computed(() => Number(authStore.globalConfig.phoneLoginStatus) === 1)
|
||||
const emailLoginStatus = computed(() => Number(authStore.globalConfig.emailLoginStatus) === 1)
|
||||
|
||||
// 定时器改变倒计时时间方法
|
||||
function changeLastSendPhoneCodeTime() {
|
||||
if (lastSendPhoneCodeTime.value > 0) {
|
||||
setTimeout(() => {
|
||||
lastSendPhoneCodeTime.value--
|
||||
changeLastSendPhoneCodeTime()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
/* 发送验证码 */
|
||||
async function handleSendCaptch() {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
const { phone, captchaCode, captchaId } = registerForm.value
|
||||
const params: any = { phone, captchaCode, captchaId }
|
||||
const res: any = await fetchSendSms(params)
|
||||
getCaptchaImg()
|
||||
const { success, message } = res
|
||||
if (success) {
|
||||
Nmessage.success(res.data)
|
||||
isSendCaptcha.value = true
|
||||
// 记录重新发送倒计时
|
||||
lastSendPhoneCodeTime.value = 60
|
||||
changeLastSendPhoneCodeTime()
|
||||
}
|
||||
else {
|
||||
isSendCaptcha.value = false
|
||||
Nmessage.error(message)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
getCaptchaImg()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 注册登录 */
|
||||
function handlerSubmit() {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
loading.value = true
|
||||
const Interface = isLogin.value ? fetchLoginByPhoneAPI : fetchRegisterByPhoneAPI
|
||||
const params: any = !isLogin.value ? registerForm.value : { phone: loginForm.value.phone, password: loginForm.value.password }
|
||||
const res: any = await Interface(params)
|
||||
loading.value = false
|
||||
getCaptchaImg()
|
||||
|
||||
const { success, message } = res
|
||||
if (!success)
|
||||
return Nmessage.error(message)
|
||||
if (!isLogin.value) {
|
||||
Nmessage.success('账户注册成功、开始体验吧!')
|
||||
const { phone, password } = registerForm.value
|
||||
loginForm.value.phone = phone
|
||||
loginForm.value.password = password
|
||||
isLogin.value = !isLogin.value
|
||||
}
|
||||
else {
|
||||
Nmessage.success('账户登录成功、开始体验吧!')
|
||||
authStore.setToken(res.data)
|
||||
authStore.getUserInfo()
|
||||
authStore.setLoginDialog(false)
|
||||
if(isMobile.value){
|
||||
window.location.reload()
|
||||
}
|
||||
ss.remove('invitedBy')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
getCaptchaImg()
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getCaptchaImg() {
|
||||
const res: any = await fetchCaptchaImg({ color: captchaBgColor.value })
|
||||
captchaSvg.value = res.data.svgCode
|
||||
registerForm.value.captchaId = res.data.code
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const code = ss.get('invitedBy')
|
||||
code && (registerForm.value.invitedBy = code)
|
||||
getCaptchaImg()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-[65px]" :class="isLogin ? 'pt-[40px]' : 'pt-5'">
|
||||
<NForm
|
||||
v-if="!isLogin"
|
||||
ref="formRef"
|
||||
:model="registerForm"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
:style="{ maxWidth: '640px' }"
|
||||
>
|
||||
<Motion :delay="50">
|
||||
<NFormItem path="username">
|
||||
<NInput v-model:value="registerForm.username" placeholder="请输入您的用户名昵称" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="120">
|
||||
<NFormItem path="password">
|
||||
<NInput v-model:value="registerForm.password" placeholder="请输入您的账户密码" type="password" :maxlength="30" show-password-on="click" tabindex="0" @keyup.enter="handlerSubmit" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="190">
|
||||
<NFormItem path="phone">
|
||||
<NInput v-model:value="registerForm.phone" placeholder="请填写您的手机号" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="260">
|
||||
<NFormItem v-if="!isSendCaptcha" path="captchaCode">
|
||||
<div class="flex items-center w-full space-x-4">
|
||||
<NInput v-model:value="registerForm.captchaCode" class="flex-1" placeholder="请填写图中验证码结果" />
|
||||
<div v-if="captchaSvg">
|
||||
<!-- <img :src="captchaSvg" alt=""> -->
|
||||
<span class="cursor-pointer rounded" @click="getCaptchaImg" v-html="captchaSvg" />
|
||||
</div>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="330">
|
||||
<NFormItem v-if="isSendCaptcha" path="phoneCode">
|
||||
<NInput v-model:value="registerForm.phoneCode" class="flex-1" placeholder="请填写手机验证码" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
|
||||
<Motion :delay="400">
|
||||
<NFormItem path="invitedBy">
|
||||
<NInput v-model:value="registerForm.invitedBy" placeholder="邀请码[非必填]" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
|
||||
<NFormItem>
|
||||
<NButton
|
||||
v-if="!isSendCaptcha"
|
||||
block
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@click="handleSendCaptch"
|
||||
>
|
||||
发送验证码
|
||||
</NButton>
|
||||
|
||||
<div v-else class="flex space-x-2 w-full">
|
||||
<NButton
|
||||
block
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
class="flex-1"
|
||||
@click="handlerSubmit"
|
||||
>
|
||||
注册账户
|
||||
</NButton>
|
||||
<NButton
|
||||
block
|
||||
class="flex-1"
|
||||
:disabled="lastSendPhoneCodeTime > 0"
|
||||
@click="isSendCaptcha = false"
|
||||
>
|
||||
重新发送{{ lastSendPhoneCodeTime ? `(${lastSendPhoneCodeTime}S)` : '' }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<!-- login -->
|
||||
<NForm
|
||||
v-if="isLogin"
|
||||
ref="formRef"
|
||||
:model="loginForm"
|
||||
size="large"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
:style="{
|
||||
maxWidth: '640px',
|
||||
}"
|
||||
>
|
||||
<Motion :delay="50">
|
||||
<NFormItem path="phone">
|
||||
<NInput v-model:value="loginForm.phone" placeholder="请输入手机号" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<Motion :delay="120">
|
||||
<NFormItem path="password">
|
||||
<NInput v-model:value="loginForm.password" placeholder="请输入您的账户密码" type="password" :maxlength="30" show-password-on="click" tabindex="0" @keyup.enter="handlerSubmit" />
|
||||
</NFormItem>
|
||||
</Motion>
|
||||
<NFormItem>
|
||||
<NButton
|
||||
block
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
class="!mt-[50px]"
|
||||
@click="handlerSubmit"
|
||||
>
|
||||
登录账户
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</div>
|
||||
<span v-if="phoneRegisterStatus" class="flex justify-center cursor-pointer">
|
||||
<NButton text @click="isLogin = !isLogin">{{ logTips }}</NButton>
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-center space-x-5 " :class="phoneRegisterStatus ? 'mt-[16px]' : 'mt-[36px]'">
|
||||
<NButton v-if="wechatRegisterStatus" ghost class="!px-10" @click="emit('changeLoginType', 'wechat')">
|
||||
<SvgIcon class="text-xl mr-2 text-[#3076fd]" icon="ph:wechat-logo" />
|
||||
微信登录
|
||||
</NButton>
|
||||
<NButton v-if="emailLoginStatus" ghost class="!px-10" @click="emit('changeLoginType', 'email')">
|
||||
<SvgIcon class="text-xl mr-2 text-[#3076fd]" icon="clarity:email-line" />
|
||||
邮箱号登录
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<Motion :delay="800">
|
||||
<div class="px-8">
|
||||
<Send v-if="isLogin" />
|
||||
</div>
|
||||
</Motion>
|
||||
</template>
|
||||
143
chat/src/layout/components/Login/Wechat.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, computed } from 'vue'
|
||||
import { NCountdown, NImage, NSkeleton, NSpin, useMessage, NButton, CountdownInst } from 'naive-ui'
|
||||
import { fetchGetQRCodeAPI, fetchGetQRSceneStrAPI, fetchLoginBySceneStrAPI } from '@/api/user'
|
||||
import type { ResData } from '@/api/types'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { ss } from '@/utils/storage'
|
||||
import Motion from '@/utils/motion/index'
|
||||
let timer: any
|
||||
import wechatIcon from '@/assets/wechat.png'
|
||||
import Send from './send.vue'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
|
||||
interface Emit {
|
||||
(ev: 'changeLoginType', val: string): void
|
||||
}
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const wxLoginUrl = ref('')
|
||||
const sceneStr = ref('')
|
||||
const activeCount = ref(false)
|
||||
const Nmessage = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const countdownRef = ref<CountdownInst | null>()
|
||||
const phoneLoginStatus = computed(() => Number(authStore.globalConfig.phoneLoginStatus) === 1)
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const emailLoginStatus = computed(() => Number(authStore.globalConfig.emailLoginStatus) === 1)
|
||||
function loadImage(src: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
|
||||
async function getSeneStr() {
|
||||
const params = { invitedBy: ss.get('invitedBy') }
|
||||
const res: ResData = await fetchGetQRSceneStrAPI(params)
|
||||
if (res.success) {
|
||||
sceneStr.value = res.data
|
||||
getQrCodeUrl()
|
||||
}
|
||||
}
|
||||
|
||||
async function loginBySnece() {
|
||||
if (!sceneStr.value)
|
||||
return
|
||||
const res: ResData = await fetchLoginBySceneStrAPI({ sceneStr: sceneStr.value })
|
||||
if (res.data) {
|
||||
clearInterval(timer)
|
||||
Nmessage.success('账户登录成功、开始体验吧!')
|
||||
authStore.setToken(res.data)
|
||||
authStore.getUserInfo()
|
||||
authStore.setLoginDialog(false)
|
||||
if(isMobile.value){
|
||||
window.location.reload()
|
||||
}
|
||||
ss.remove('invitedBy')
|
||||
}
|
||||
}
|
||||
|
||||
async function getQrCodeUrl() {
|
||||
const res: ResData = await fetchGetQRCodeAPI({ sceneStr: sceneStr.value })
|
||||
if (res.success) {
|
||||
activeCount.value = true
|
||||
await loadImage(res.data)
|
||||
wxLoginUrl.value = res.data
|
||||
timer = setInterval(() => {
|
||||
loginBySnece()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimeDown() {
|
||||
clearInterval(timer)
|
||||
getSeneStr()
|
||||
countdownRef.value?.reset()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSeneStr()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col items-center">
|
||||
<div class="text-[#374151] dark:text-white font-bold text-[20px] mt-[50px]">微信扫码登录</div>
|
||||
<div style="white-space: nowrap" class="mt-[20px] w-full text-center font-bold text-sm">
|
||||
<p>
|
||||
<span class="w-[65px] inline-block font-normal text-[#FF505C] text-left"><NCountdown ref="countdownRef" :active="activeCount" :duration="60 * 1000" :on-finish="handleTimeDown" /></span> 秒后二维码将刷新
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Motion :delay="200" :scale="0.5" :duration="500">
|
||||
<div class="w-[280px] h-[280px] wechat-shadow flex flex-col justify-center items-center relative select-none mt-[20px]">
|
||||
<NImage
|
||||
v-if="wxLoginUrl"
|
||||
preview-disabled
|
||||
class="w-[220px] h-[220px] select-none"
|
||||
:src="wxLoginUrl"
|
||||
/>
|
||||
<NSkeleton v-else height="230px" width="220px" animated />
|
||||
<NSpin v-if="!wxLoginUrl" size="large" class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2" />
|
||||
|
||||
<div class="mt-2 text-[#222222] dark:text-white font-normal flex items-center">
|
||||
<img :src="wechatIcon" class="w-[16px] mr-1" alt="">
|
||||
微信扫码
|
||||
</div>
|
||||
</div>
|
||||
</Motion>
|
||||
<Motion :delay="200">
|
||||
<div class="flex items-center justify-center space-x-5 mt-[36px] ">
|
||||
<n-button v-if="emailLoginStatus" ghost class="!px-10" @click="emit('changeLoginType', 'email')">
|
||||
<SvgIcon class="text-xl mr-2 text-[#3076fd]" icon="clarity:email-line" />
|
||||
邮箱号登录
|
||||
</n-button>
|
||||
<n-button v-if="phoneLoginStatus" ghost class="!px-10" @click="emit('changeLoginType', 'phone')">
|
||||
<SvgIcon class="text-xl mr-2 text-[#3076fd]" icon="clarity:mobile-phone-solid" />
|
||||
手机号登录
|
||||
</n-button>
|
||||
</div>
|
||||
</Motion>
|
||||
|
||||
<Motion :delay="400">
|
||||
<Send/>
|
||||
</Motion>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style>
|
||||
.wechat-shadow{
|
||||
box-shadow: 0px 8px 10px 1px rgba(0,0,0,0.1608);
|
||||
}
|
||||
</style>
|
||||
27
chat/src/layout/components/Login/send.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
|
||||
<script lang='ts' setup>
|
||||
import { useAuthStore } from '@/store'
|
||||
import { computed } from 'vue'
|
||||
import { NAlert} from 'naive-ui'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
|
||||
const registerSendStatus = computed(() => Number(authStore.globalConfig.registerSendStatus))
|
||||
const registerSendModel3Count = computed(() => Number(authStore.globalConfig.registerSendModel3Count))
|
||||
const registerSendModel4Count = computed(() => Number(authStore.globalConfig.registerSendModel4Count))
|
||||
const registerSendDrawMjCount = computed(() => Number(authStore.globalConfig.registerSendDrawMjCount))
|
||||
const registerTips = computed(() => (`首次认证:赠送${registerSendModel3Count.value}积分基础模型余额 | ${registerSendModel4Count.value}积分高级模型余额 | ${registerSendDrawMjCount.value}积分绘画余额`))
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="registerSendStatus">
|
||||
<NAlert type="error" :show-icon="false" class="mt-5">
|
||||
{{ registerTips }}
|
||||
</NAlert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
98
chat/src/layout/components/NoticeDialog.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup lang='ts'>
|
||||
import { NButton, NCard, NModal, NSkeleton, NSpace } from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAppStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { fetchGetGlobalNoticeAPI } from '@/api/global'
|
||||
|
||||
import type { ResData } from '@/api/types'
|
||||
import { ss } from '@/utils/storage'
|
||||
defineProps<Props>()
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Notice {
|
||||
noticeInfo: string
|
||||
noticeTitle: string
|
||||
}
|
||||
|
||||
const notice = ref<Notice>({
|
||||
noticeInfo: '',
|
||||
noticeTitle: '',
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const loading = ref(true)
|
||||
const darkMode = computed(() => appStore.theme === 'dark')
|
||||
const { isSmallLg } = useBasicLayout()
|
||||
const theme = computed(() => appStore.theme)
|
||||
|
||||
const html = computed(() => {
|
||||
if (!notice.value.noticeInfo)
|
||||
return ''
|
||||
return marked(notice.value.noticeInfo)
|
||||
})
|
||||
|
||||
function handleCloseDialog() {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
useGlobalStore.updateNoticeDialog(false)
|
||||
}
|
||||
|
||||
async function queryNotice() {
|
||||
const res: ResData = await fetchGetGlobalNoticeAPI()
|
||||
const { success, data } = res
|
||||
if (success)
|
||||
notice.value = data
|
||||
}
|
||||
|
||||
async function openDrawerAfter() {
|
||||
await queryNotice()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function handleReminder() {
|
||||
useGlobalStore.updateNoticeDialog(false)
|
||||
ss.set('showNotice', Date.now() + 24 * 60 * 60 * 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" class="p-0 noticeDialog" :style="{ maxWidth: '780px', minWidth: isSmallLg ? '100%' : '780px' }" :on-after-enter="openDrawerAfter" :on-after-leave="handleCloseDialog">
|
||||
<NSpace vertical>
|
||||
<NCard closable @close="handleClose">
|
||||
<template #header>
|
||||
<div v-if="loading" class="px-[20px]" >
|
||||
<NSkeleton text width="30%" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<span class="text-xl">{{ notice.noticeTitle }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="loading" class="px-[20px]" >
|
||||
<NSkeleton text :repeat="10" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div :class="[darkMode ? 'text-[#fff]' : 'text-[#000]', 'pb-5']" :style="{ background: theme === 'dark' ? '#2c2c32' : '#fff' }" class="p-[20px] markdown-body markdown-body-generate max-h-[500px] overflow-y-auto overflow-x-hidden" v-html="html" />
|
||||
</template>
|
||||
<div class="flex justify-end py-3 px-5">
|
||||
<NButton type="primary" @click="handleReminder">
|
||||
24小时不再提示
|
||||
</NButton>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/deep/ .n-card__content{
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
267
chat/src/layout/components/PayDialog.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<script setup lang='ts'>
|
||||
import type { CountdownInst } from 'naive-ui'
|
||||
import { NButton, NCountdown, NIcon, NModal, NRadio, NRadioGroup, NSkeleton, NSpace, NSpin, useMessage } from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { CloseOutline, PaperPlaneOutline } from '@vicons/ionicons5'
|
||||
import { useAuthStore, useGlobalStore } from '@/store'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { fetchOrderBuyAPI, fetchOrderQueryAPI } from '@/api/order'
|
||||
|
||||
import type { ResData } from '@/api/types'
|
||||
import QRCode from '@/components/common/QRCode/index.vue'
|
||||
import alipay from '@/assets/alipay.png'
|
||||
import wxpay from '@/assets/wxpay.png'
|
||||
defineProps<Props>()
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const useGlobal = useGlobalStore()
|
||||
const POLL_INTERVAL = 1000
|
||||
const ms = useMessage()
|
||||
const active = ref(true)
|
||||
const payType = ref('alipay') // 默认支付宝支付
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
/* 是否是微信环境 */
|
||||
const isWxEnv = computed(() => {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
return ua.match(/MicroMessenger/i) && ua?.match(/MicroMessenger/i)?.[0] === 'micromessenger'
|
||||
})
|
||||
|
||||
/* 开启的支付平台 */
|
||||
const payPlatform = computed(() => {
|
||||
const { payHupiStatus, payEpayStatus, payMpayStatus, payWechatStatus } = authStore.globalConfig
|
||||
if (Number(payWechatStatus) === 1)
|
||||
return 'wechat'
|
||||
|
||||
if (Number(payEpayStatus) === 1)
|
||||
return 'epay'
|
||||
|
||||
if (Number(payMpayStatus) === 1)
|
||||
return 'mpay'
|
||||
|
||||
if (Number(payHupiStatus) === 1)
|
||||
return 'hupi'
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
/* 支付平台开启的支付渠道 */
|
||||
const payChannel = computed(() => {
|
||||
const { payEpayChannel, payMpayChannel } = authStore.globalConfig
|
||||
if (payPlatform.value === 'mpay')
|
||||
return payMpayChannel ? JSON.parse(payMpayChannel) : []
|
||||
|
||||
if (payPlatform.value === 'epay')
|
||||
return payEpayChannel ? JSON.parse(payEpayChannel) : []
|
||||
|
||||
if (payPlatform.value === 'wechat')
|
||||
return ['wxpay']
|
||||
|
||||
if (payPlatform.value === 'hupi')
|
||||
return ['wxpay']
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const plat = computed(() => payType.value === 'wxpay' ? '微信' : '支付宝')
|
||||
const countdownRef = ref<CountdownInst | null>()
|
||||
|
||||
const isRedirectPay = computed(() => {
|
||||
const { payEpayApiPayUrl } = authStore.globalConfig
|
||||
return (payPlatform.value === 'epay' && payEpayApiPayUrl.includes('submit')) || payPlatform.value === 'mpay'
|
||||
})
|
||||
|
||||
watch(payType, () => {
|
||||
getQrCode()
|
||||
countdownRef.value?.reset()
|
||||
})
|
||||
|
||||
const orderId = ref('')
|
||||
let timer: any
|
||||
const payTypes = computed(() => {
|
||||
return [
|
||||
{ label: '微信支付', value: 'wxpay', icon: wxpay, payChannel: 'wxpay' },
|
||||
{ label: '支付宝支付', value: 'alipay', icon: alipay, payChannel: 'alipay' },
|
||||
].filter(item => payChannel.value.includes(item.payChannel))
|
||||
})
|
||||
|
||||
const queryOrderStatus = async () => {
|
||||
if (!orderId.value)
|
||||
return
|
||||
const result: ResData = await fetchOrderQueryAPI({ orderId: orderId.value })
|
||||
const { success, data } = result
|
||||
if (success) {
|
||||
const { status } = data
|
||||
if (status === 1) {
|
||||
clearInterval(timer)
|
||||
ms.success('恭喜你支付成功、祝您使用愉快!')
|
||||
active.value = false
|
||||
authStore.getUserInfo()
|
||||
setTimeout(() => {
|
||||
useGlobal.updatePayDialog(false)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const orderInfo = computed(() => useGlobal?.orderInfo)
|
||||
const url_qrcode = ref('')
|
||||
const qrCodeloading = ref(true)
|
||||
const redirectloading = ref(true)
|
||||
const redirectUrl = ref('')
|
||||
|
||||
function handleCloseDialog() {
|
||||
useGlobal.updateOrderInfo({})
|
||||
clearInterval(timer)
|
||||
}
|
||||
|
||||
/* 请求二维码 */
|
||||
async function getQrCode() {
|
||||
!isRedirectPay.value && (qrCodeloading.value = true)
|
||||
isRedirectPay.value && (redirectloading.value = true)
|
||||
let qsPayType = null
|
||||
qsPayType = payType.value
|
||||
if (payPlatform.value === 'wechat')
|
||||
qsPayType = isWxEnv.value ? 'jsapi' : 'native'
|
||||
|
||||
try {
|
||||
const res: ResData = await fetchOrderBuyAPI({ goodsId: orderInfo.value.pkgInfo.id, payType: qsPayType })
|
||||
const { data, success, message } = res
|
||||
if (!success)
|
||||
return ms.error(message)
|
||||
|
||||
const { url_qrcode: code, orderId: id, redirectUrl: url } = data
|
||||
redirectUrl.value = url
|
||||
orderId.value = id
|
||||
url_qrcode.value = code
|
||||
qrCodeloading.value = false
|
||||
redirectloading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
useGlobal.updatePayDialog(false)
|
||||
qrCodeloading.value = false
|
||||
redirectloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* 跳转支付 */
|
||||
function handleRedPay() {
|
||||
window.open(redirectUrl.value)
|
||||
}
|
||||
|
||||
async function handleOpenDialog() {
|
||||
await getQrCode()
|
||||
timer = setInterval(() => {
|
||||
queryOrderStatus()
|
||||
}, POLL_INTERVAL)
|
||||
}
|
||||
|
||||
function handleFinish() {
|
||||
ms.error('支付超时,请重新下单!')
|
||||
clearInterval(timer)
|
||||
useGlobal.updatePayDialog(false)
|
||||
// useGlobal.updateGoodsDialog(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" style="width: 90%; max-width: 750px" :on-after-enter="handleOpenDialog" :on-after-leave="handleCloseDialog">
|
||||
<div class="p-4 bg-white rounded dark:bg-slate-800">
|
||||
<div class="flex justify-between" @click="useGlobal.updatePayDialog(false)">
|
||||
<div class="flex text-xl font-bold mb-[20px] bg-currentflex items-center ">
|
||||
<NIcon size="25" color="#0e7a0d">
|
||||
<PaperPlaneOutline />
|
||||
</NIcon>
|
||||
<span class="ml-[8px]">商品支付</span>
|
||||
</div>
|
||||
<NIcon size="20" color="#0e7a0d" class="cursor-pointer">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
</div>
|
||||
<div class="p-4 ">
|
||||
<div><span class="whitespace-nowrap font-bold">需要支付:</span> <i class="text-xl text-[red] font-bold">{{ `¥${orderInfo.pkgInfo?.price}` }}</i></div>
|
||||
<div class="mt-2 flex">
|
||||
<span class="whitespace-nowrap font-bold">套餐名称:</span><span class="ml-2"> {{ orderInfo.pkgInfo?.name }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex">
|
||||
<span class="whitespace-nowrap font-bold">套餐描述:</span><span class="ml-2"> {{ orderInfo.pkgInfo?.des }} </span>
|
||||
</div>
|
||||
<!-- <div class="flex mt-3">
|
||||
<span class="whitespace-nowrap font-bold">套餐详情:</span>
|
||||
<div class="flex flex-col space-y-2 pl-2 w-full ">
|
||||
<div class="flex justify-between w-[300px] ">
|
||||
<span>基础模型额度</span>
|
||||
<span>100 次</span>
|
||||
</div>
|
||||
<div class="flex justify-between w-[300px] ">
|
||||
<span>基础模型额度</span>
|
||||
<span>20 次</span>
|
||||
</div>
|
||||
<div class="flex justify-between w-[300px] ">
|
||||
<span>MJ绘画额度</span>
|
||||
<span>20 次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="flex justify-center" :class="[isMobile ? 'flex-col' : 'flex-row', isRedirectPay ? 'flex-row-reverse' : '']">
|
||||
<div>
|
||||
<!-- <div style="white-space: nowrap" class="mt-6 w-full text-center font-bold text-sm">
|
||||
请在 <span class="w-[60px] inline-block text-[red] text-left"><NCountdown :active="active" :duration="300 * 1000" :on-finish="handleFinish" /></span> 时间内完成支付!
|
||||
</div> -->
|
||||
<div class="flex items-center justify-center my-3 relative ">
|
||||
<!-- qrCodeloading -->
|
||||
<NSpin v-if="qrCodeloading && !isRedirectPay" size="large" class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2" />
|
||||
<NSkeleton v-if="qrCodeloading" :width="240" :height="240" :sharp="false" size="medium" />
|
||||
|
||||
<!-- epay -->
|
||||
<QRCode v-if="payPlatform === 'epay' && !qrCodeloading && !redirectloading && !isRedirectPay" :value="url_qrcode" :size="240" />
|
||||
<img v-if="payType === 'wxpay' && !qrCodeloading && !isRedirectPay" :src="wxpay" class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-10 bg-[#fff]">
|
||||
<img v-if="payType === 'alipay' && !qrCodeloading && !isRedirectPay" :src="alipay" class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-10 bg-[#fff]">
|
||||
|
||||
<!-- wechat -->
|
||||
<QRCode v-if="payPlatform === 'wechat' && !qrCodeloading" :value="url_qrcode" :size="240" />
|
||||
|
||||
<div v-if="isRedirectPay" class="flex flex-col" :class="[isRedirectPay && isMobile ? 'ml-0' : 'ml-20']">
|
||||
<span class="mb-10 mt-5 text-base">当前站长开通了跳转支付</span>
|
||||
|
||||
<!-- mapy 跳转支付 -->
|
||||
<NButton v-if="isRedirectPay" type="primary" ghost :disabled="redirectloading" :loading="redirectloading" @click="handleRedPay">
|
||||
点击前往支付
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<!-- hupi -->
|
||||
<iframe v-if="payPlatform === 'hupi' && !redirectloading" class="w-[280px] h-[280px] scale-90" :src="url_qrcode" frameborder="0" />
|
||||
</div>
|
||||
<span v-if="!isRedirectPay" class="flex items-center justify-center text-lg ">
|
||||
{{ `打开${plat}扫码支付` }}
|
||||
</span>
|
||||
</div>
|
||||
<div class=" flex flex-col" :class="[isMobile ? 'w-full ' : ' ml-10 w-[200] ']">
|
||||
<!-- <h4 class="mb-10 font-bold text-lg">
|
||||
支付方式
|
||||
</h4> -->
|
||||
<div style="white-space: nowrap" class="mt-6 w-full text-center font-bold text-sm" :class="[isMobile ? 'mb-2' : 'mb-10']">
|
||||
请在 <span class="w-[60px] inline-block text-[red] text-left"><NCountdown ref="countdownRef" :active="active" :duration="300 * 1000" :on-finish="handleFinish" /></span> 时间内完成支付!
|
||||
</div>
|
||||
<NRadioGroup v-model:value="payType" name="radiogroup" class="flex">
|
||||
<NSpace :vertical="!isMobile" justify="center" :size="isMobile ? 10 : 35" class="w-full">
|
||||
<NRadio v-for="pay in payTypes" :key="pay.value" :value="pay.value">
|
||||
<div class="flex items-center">
|
||||
<img class="h-4 object-contain mr-2" :src="pay.icon" alt=""> {{ pay.label }}
|
||||
</div>
|
||||
</NRadio>
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
134
chat/src/layout/components/SignInDialog.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script setup lang='ts'>
|
||||
import { NAlert, NButton, NCalendar, NCard, NModal, NSpace, NSpin, useMessage } from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { addDays, isThisMonth } from 'date-fns/esm'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAppStore, useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { fetchSignInAPI, fetchSignLogAPI } from '@/api/signin'
|
||||
import type { ResData } from '@/api/types'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
defineProps<Props>()
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const loading = ref(false)
|
||||
const { isMobile } = useBasicLayout()
|
||||
const signInData = ref([])
|
||||
const ms = useMessage()
|
||||
const { isSmallLg } = useBasicLayout()
|
||||
// const value = ref(addDays(Date.now(), 1).valueOf())
|
||||
const value = undefined
|
||||
const signInLoading = ref(false)
|
||||
|
||||
function handleCloseDialog() {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
useGlobalStore.updateSignInDialog(false)
|
||||
}
|
||||
|
||||
function isDateDisabled(timestamp: number) {
|
||||
if (!isThisMonth(timestamp))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
/* 连续签到天数 */
|
||||
const consecutiveDays = computed(() => authStore.userInfo.consecutiveDays)
|
||||
const signInModel3Count = computed(() => Number(authStore.globalConfig?.signInModel3Count) || 0)
|
||||
const signInModel4Count = computed(() => Number(authStore.globalConfig?.signInModel4Count) || 0)
|
||||
const signInMjDrawToken = computed(() => Number(authStore.globalConfig?.signInMjDrawToken) || 0)
|
||||
|
||||
function signed(month: number, date: number) {
|
||||
if (!signInData.value.length)
|
||||
return false
|
||||
const str = `${new Date().getFullYear()}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`
|
||||
const res: any = signInData.value.find((item: any) => item.signInDate === str)
|
||||
return !res ? false : res?.isSigned
|
||||
}
|
||||
|
||||
const hasSignedInToday = computed(() => {
|
||||
if (loading.value)
|
||||
return false
|
||||
const month = new Date().getMonth() + 1
|
||||
const date = new Date().getDate()
|
||||
return !signed(month, date)
|
||||
})
|
||||
|
||||
async function getSigninLog() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: ResData = await fetchSignLogAPI()
|
||||
signInData.value = res.data
|
||||
loading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSignIn() {
|
||||
try {
|
||||
signInLoading.value = true
|
||||
const res: ResData = await fetchSignInAPI()
|
||||
if (res.success)
|
||||
ms.success('签到成功!')
|
||||
getSigninLog()
|
||||
authStore.getUserInfo()
|
||||
signInLoading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
signInLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openDrawerAfter() {
|
||||
getSigninLog()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" :style="{ maxWidth: '650px', minWidth: isSmallLg ? '100%' : '650px' }" :on-after-enter="openDrawerAfter" :on-after-leave="handleCloseDialog">
|
||||
<NSpace vertical>
|
||||
<NCard closable @close="handleClose">
|
||||
<template #header>
|
||||
<span class="text-base">签到奖励 <span>(已连续签到<b class="text-[red]">{{ consecutiveDays }}</b>天)</span></span>
|
||||
</template>
|
||||
<NAlert class="mb-5 p-0 !bg-[#ccddff]" :show-icon="false" type="primary">
|
||||
每日签到赠送:
|
||||
<span v-if="signInModel3Count > 0"><b class=" text-[red]">{{ signInModel3Count }}</b>积分基础模型对话额度</span>
|
||||
<span v-if="signInModel4Count > 0"><b class="ml-2 text-[red]">{{ signInModel4Count }}</b>积分高级模型对话额度</span>
|
||||
<span v-if="signInMjDrawToken > 0"><b class="ml-2 text-[red]">{{ signInMjDrawToken }}</b>点绘画积分额度</span>
|
||||
</NAlert>
|
||||
<NSpin :show="loading">
|
||||
<NCalendar v-model:value="value" style="height:420px" #="{ month, date }" :is-date-disabled="isDateDisabled">
|
||||
<div v-if="signed(month, date)" class="flex items-center w-full mt-2">
|
||||
<SvgIcon icon="heroicons:gift" class="text-xl text-[#5A91FC]" />
|
||||
<span v-if="!isMobile" class="ml-2 text-xs">已签到</span>
|
||||
</div>
|
||||
</NCalendar>
|
||||
</NSpin>
|
||||
<div v-if="hasSignedInToday" class="flex mt-3 w-full mt-14">
|
||||
<NButton style="width: 100%" type="primary" round :loading="signInLoading" @click="handleSignIn">
|
||||
今日尚未签到、点击签到
|
||||
</NButton>
|
||||
</div>
|
||||
<div v-if="!hasSignedInToday" class="flex mt-8 w-full mt-14">
|
||||
<NButton style="width: 100%" type="primary" round :loading="signInLoading" >
|
||||
今日已成功签到
|
||||
</NButton>
|
||||
</div>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.n-calendar-header__extra{
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
313
chat/src/layout/components/modelDialog.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<script setup lang='ts'>
|
||||
import { NCountdown, NIcon, NImage, NModal, NSkeleton, NSpin, useMessage, NInput, NSelect, NCascader, NCollapse, NCollapseItem, NButton, NSlider, NTooltip, NTag } from 'naive-ui'
|
||||
import { ref, onMounted, computed, watch, h } from 'vue'
|
||||
import { CloseOutline, SettingsOutline } from '@vicons/ionicons5'
|
||||
import { fetchQueryModelsListAPI } from '@/api/models'
|
||||
import { useAuthStore, useGlobalStoreWithOut, useChatStore } from '@/store'
|
||||
import { fetchUpdateGroupAPI } from '@/api/group'
|
||||
|
||||
defineProps<Props>()
|
||||
interface ModelType {
|
||||
label: string
|
||||
val: number
|
||||
}
|
||||
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const authStore = useAuthStore()
|
||||
const chatStore = useChatStore()
|
||||
const loading = ref(false)
|
||||
|
||||
/* 当前对话组的配置信息 */
|
||||
const activeConfig = computed(() => {
|
||||
return chatStore.activeConfig
|
||||
})
|
||||
|
||||
const activeGroupAppId = computed(() => chatStore.activeGroupAppId )
|
||||
|
||||
/* 不是openai的模型暂时不让设置预设 */
|
||||
const disabled = computed(() => {
|
||||
return Number(activeConfig.value?.modelTypeInfo?.val) !== 1 || Number(activeGroupAppId.value) > 0
|
||||
})
|
||||
|
||||
/* 温度 */
|
||||
const maxTemperature = computed(() => {
|
||||
return Number(chatStore.activeModelKeyType) === 1 ? 1.2 : 1
|
||||
})
|
||||
|
||||
/* 当前的对话组id */
|
||||
const chatGroupId = computed(() => chatStore.active)
|
||||
|
||||
watch(activeConfig, (val) => {
|
||||
if (!val) return;
|
||||
compilerConfig(val)
|
||||
})
|
||||
|
||||
const maxModelTokens = ref(0)
|
||||
const maxResponseTokens = ref(0)
|
||||
const topN = ref(0.8)
|
||||
const modelTypes = ref<ModelType[]>([])
|
||||
const model = ref('')
|
||||
const systemMessage = ref('')
|
||||
const maxRounds = ref()
|
||||
const rounds = ref(8)
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const message = useMessage()
|
||||
const showResetBtn = ref(false)
|
||||
let modelMapsCache: any = ref({})
|
||||
let modelTypeListCache: any = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
queryModelsList()
|
||||
})
|
||||
|
||||
function compilerConfig(val: any){
|
||||
const { modelInfo, modelTypeInfo } = val
|
||||
if (!modelInfo || !modelTypeInfo) return;
|
||||
maxModelTokens.value = modelInfo.maxModelTokens
|
||||
maxResponseTokens.value = modelInfo.maxResponseTokens
|
||||
topN.value = modelInfo.topN
|
||||
systemMessage.value = modelInfo.systemMessage
|
||||
model.value = `${modelTypeInfo.val}----${modelInfo.model}`
|
||||
maxRounds.value = modelInfo.maxRounds
|
||||
rounds.value = modelInfo.rounds > modelInfo.maxRounds ? modelInfo.maxRounds : modelInfo.rounds
|
||||
}
|
||||
|
||||
/* 应用只可以使用openai模型 */
|
||||
const options = computed(() => {
|
||||
const data = !activeGroupAppId.value ? modelTypeListCache : modelTypeListCache.filter( (item: any) => Number(item.val) === 1 )
|
||||
return data.map((item: any) => {
|
||||
const { label, val } = item
|
||||
return {
|
||||
label,
|
||||
value: val,
|
||||
children: modelMapsCache[val].map((item: any) => {
|
||||
const { model, modelName } = item
|
||||
return {
|
||||
label: modelName,
|
||||
value: `${val}----${model}`
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
async function queryModelsList() {
|
||||
try {
|
||||
const res: any = await fetchQueryModelsListAPI()
|
||||
if (!res.success) return
|
||||
const { modelMaps, modelTypeList } = res.data
|
||||
modelMapsCache = modelMaps
|
||||
modelTypeListCache = modelTypeList
|
||||
// options.value = modelTypeList.map((item: any) => {
|
||||
// const { label, val } = item
|
||||
// return {
|
||||
// label,
|
||||
// value: val,
|
||||
// children: modelMaps[val].map((item: any) => {
|
||||
// const { model, modelName } = item
|
||||
// return {
|
||||
// label: modelName,
|
||||
// value: `${val}----${model}`
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
modelTypes.value = modelTypeList;
|
||||
// const typeValue = modelTypes.value[0].val
|
||||
/* 设置默认为第一项 使用 ---- 分割 前面是 模型类型 后面是模型的名称 */
|
||||
// model.value = `${modelTypes.value[0].val}----${modelMaps[typeValue][0].model}`
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
queryModelsList()
|
||||
}
|
||||
|
||||
async function handleReset() {
|
||||
const config = chatStore.baseConfig
|
||||
compilerConfig(config)
|
||||
}
|
||||
|
||||
function handleUpdate(val: any) {
|
||||
showResetBtn.value = val.includes('1')
|
||||
}
|
||||
|
||||
/* 获取模型的单项信息 */
|
||||
function getModelTypeInfo(type: any) {
|
||||
return modelTypeListCache.find((item: any) => item.val === type)
|
||||
}
|
||||
|
||||
/* 获取模型名称 */
|
||||
function getModelDetailInfo(type: any, model: any) {
|
||||
return modelMapsCache[type].find((item: any) => item.model === model);
|
||||
}
|
||||
|
||||
|
||||
/* 修改对话组模型配置 */
|
||||
async function handleUpdateConfig() {
|
||||
const [type, m] = model.value.split('----')
|
||||
const { maxModelTokens } = activeConfig.value.modelInfo
|
||||
const selectModelInfo = getModelDetailInfo(type, m)
|
||||
const { modelName, deductType, deduct, maxRounds } = selectModelInfo
|
||||
const config = {
|
||||
modelInfo: {
|
||||
keyType: type,
|
||||
modelName,
|
||||
model: m,
|
||||
maxModelTokens: maxModelTokens,
|
||||
maxResponseTokens: maxResponseTokens.value,
|
||||
systemMessage: systemMessage?.value,
|
||||
topN: topN.value,
|
||||
deductType,
|
||||
deduct,
|
||||
maxRounds,
|
||||
rounds: rounds.value
|
||||
},
|
||||
modelTypeInfo: getModelTypeInfo(type)
|
||||
}
|
||||
|
||||
const params = {
|
||||
groupId: chatGroupId.value,
|
||||
config: JSON.stringify(config)
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
await fetchUpdateGroupAPI(params)
|
||||
loading.value = false
|
||||
message.success('修改当前对话组自定义模型配置成功!')
|
||||
await chatStore.queryMyGroup()
|
||||
useGlobalStore.updateModelDialog(false)
|
||||
} catch (error) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function renderLabel(option: { value?: string | number; label?: string }) {
|
||||
return () => h(NTooltip, { placement: 'bottom', trigger: 'hover' },
|
||||
[
|
||||
h(
|
||||
'template',
|
||||
{ slot: 'trigger' },
|
||||
h('span', null, option.label)
|
||||
),
|
||||
h(
|
||||
'span',
|
||||
null,
|
||||
option.label
|
||||
)
|
||||
]
|
||||
);
|
||||
// return `prefix ${option.label}`
|
||||
}
|
||||
|
||||
function handleCloseDialog() {
|
||||
showResetBtn.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" style="width: 90%; max-width: 650px" :on-after-enter="openDialog"
|
||||
:on-after-leave="handleCloseDialog">
|
||||
<div class="py-3 px-5 bg-white rounded dark:bg-slate-800">
|
||||
<div class="absolute top-3 right-3 cursor-pointer" @click="useGlobalStore.updateModelDialog(false)">
|
||||
<NIcon size="20" color="#0e7a0d">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
</div>
|
||||
<div class="flex font-bold mb-[20px] bg-currentflex items-center ">
|
||||
<NIcon size="24" color="#0e7a0d">
|
||||
<SettingsOutline />
|
||||
</NIcon>
|
||||
|
||||
<span class="ml-[8px] mt-1 text-lg">模型个性化</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-6 pb-4">
|
||||
<span class="font-bold">模型选用</span>
|
||||
<div style="max-width:70%">
|
||||
<n-cascader class="w-full" v-model:value="model" placeholder="请选用当前聊天组所需的模型!" expand-trigger="click"
|
||||
:options="options" check-strategy="child" :show-path="true" :filterable="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="pb-1">自定义角色预设</div>
|
||||
<n-input v-model:value="systemMessage" type="textarea" :disabled="disabled" placeholder="自定义头部预设、给你的AI预设一个身份、更多有趣的角色请前往「应用广场」..." />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 bg-[#fafbfc] px-2 py-2 dark:bg-[#243147]">
|
||||
<n-collapse default-expanded-names="" accordion :on-update:expanded-names="handleUpdate">
|
||||
<n-collapse-item name="1">
|
||||
<template #header>
|
||||
<div>
|
||||
高级配置
|
||||
<span class="text-xs text-neutral-500">(不了解不需要修改)</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<div @click.stop="handleReset">
|
||||
<NButton text type="error" v-if="showResetBtn">
|
||||
重置
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mt-2">
|
||||
<div>
|
||||
<div class=" w-full flex justify-between">
|
||||
<span class="w-[150px]">话题随机性</span>
|
||||
<div class="flex w-[200px] items-center">
|
||||
<n-slider v-model:value="topN" :step="0.1" :max="maxTemperature" />
|
||||
<span class="w-[55px] text-right">
|
||||
{{ topN }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-500 dark:text-slate-400">较高的数值会使同问题每次输出的结果更随机</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class=" w-full flex justify-between">
|
||||
<span class="w-[150px]">回复Token数</span>
|
||||
<div class="flex w-[200px] items-center">
|
||||
<n-slider v-model:value="maxResponseTokens" :step="100" :max="maxModelTokens" />
|
||||
<span class="w-[55px] text-right">
|
||||
{{ maxResponseTokens }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-500 dark:text-slate-400">单条回复数,但也会消耗更多的额度</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class=" w-full flex justify-between">
|
||||
<span class="w-[150px]">关联上下文数量</span>
|
||||
<div class="flex w-[200px] items-center">
|
||||
<n-slider v-model:value="rounds" :step="1" :max="maxRounds" />
|
||||
<span class="w-[55px] text-right">
|
||||
{{ rounds }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-500 dark:text-slate-400">单条回复数,但也会消耗更多的额度</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center justify-end space-x-4">
|
||||
<NButton @click="useGlobalStore.updateModelDialog(false)">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleUpdateConfig" :loading="loading">
|
||||
保存
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
93
chat/src/layout/footerBar/index.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, onBeforeMount, ref } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { fetchQueryMenuAPI } from '@/api/config'
|
||||
|
||||
interface MenuItem {
|
||||
id: number
|
||||
menuName: string
|
||||
menuPath: string
|
||||
menuIcon: string
|
||||
menuTipText: string
|
||||
menuIframeUrl: string
|
||||
isJump: boolean
|
||||
isNeedAuth: boolean
|
||||
}
|
||||
const menuLista = ref<MenuItem[]>([])
|
||||
const message = useMessage()
|
||||
|
||||
async function queryMenu() {
|
||||
const res: any = await fetchQueryMenuAPI({ menuPlatform: 0 })
|
||||
if (!res.success)
|
||||
return
|
||||
menuLista.value = res.data
|
||||
}
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const activeRoutePath = computed(() => route.path)
|
||||
const authStore = useAuthStore()
|
||||
const iframeSrc = computed(() => useGlobalStore.iframeUrl)
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
|
||||
function handleToPage(menu: MenuItem) {
|
||||
const { menuPath, isJump, menuIframeUrl, isNeedAuth } = menu
|
||||
if (isNeedAuth && !isLogin.value) {
|
||||
message.warning('请先登录后访问!')
|
||||
authStore.setLoginDialog(true)
|
||||
return
|
||||
}
|
||||
useGlobalStore.updateIframeUrl('')
|
||||
if (menuPath) {
|
||||
return router.push({ path: menuPath })
|
||||
}
|
||||
else {
|
||||
if (isJump) {
|
||||
window.open(menuIframeUrl)
|
||||
}
|
||||
else {
|
||||
useGlobalStore.updateIframeUrl(menuIframeUrl)
|
||||
router.push({ path: '/extend' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isActive(item: MenuItem) {
|
||||
const { menuIframeUrl, menuPath } = item
|
||||
if (menuIframeUrl)
|
||||
return menuIframeUrl === iframeSrc.value
|
||||
|
||||
if (menuPath)
|
||||
return menuPath === activeRoutePath.value
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
queryMenu()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="bg-white dark:bg-[#25272c]">
|
||||
<div
|
||||
class="grid border-t py-1 dark:border-t-neutral-800 grid-cols-2"
|
||||
:style="{
|
||||
gridTemplateColumns: `repeat(${menuLista.length}, minmax(0, 1fr))`,
|
||||
}"
|
||||
>
|
||||
<a
|
||||
v-for="item in menuLista"
|
||||
:key="item.id"
|
||||
:class="[isActive(item) ? 'text-[#4b9e5f] dark:text-[#86dfba]' : '']"
|
||||
class="cursor-pointer text-center leading-4"
|
||||
@click="handleToPage(item)"
|
||||
>
|
||||
<span class="inline-block text-xl">
|
||||
<SvgIcon :icon="item.menuIcon" class="mb-1 inline-block text-lg" /></span>
|
||||
<p class="text-xs">{{ item.menuTipText }}</p>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
3
chat/src/layout/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Layout from './index.vue'
|
||||
|
||||
export { Layout }
|
||||
125
chat/src/layout/index.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { NLayoutContent, useMessage } from 'naive-ui'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import SiderBar from './siderBar/index.vue'
|
||||
import FooterBar from './footerBar/index.vue'
|
||||
import Login from './components/Login.vue'
|
||||
import PayDialog from './components/PayDialog.vue'
|
||||
import GoodsDialog from './components/GoodsDialog.vue'
|
||||
import NoticeDialog from './components/NoticeDialog.vue'
|
||||
import BindWxDialog from './components/BindWx.vue'
|
||||
import SignInDialog from './components/SignInDialog.vue'
|
||||
import ModelDialog from './components/modelDialog.vue'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAppStore, useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { fetchLoginByCodeAPI, fetchWxLoginRedirectAPI } from '@/api/user'
|
||||
import Loading from '@/components/base/Loading.vue'
|
||||
import type { ResData } from '@/api/types'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const appStore = useAppStore()
|
||||
const ms = useMessage()
|
||||
const payDialog = computed(() => useGlobalStore.payDialog)
|
||||
const goodsDialog = computed(() => useGlobalStore.goodsDialog)
|
||||
const noticeDialog = computed(() => useGlobalStore.noticeDialog)
|
||||
const bindWxDialog = computed(() => useGlobalStore.bindWxDialog)
|
||||
const signInDialog = computed(() => useGlobalStore.signInDialog)
|
||||
const modelDialog = computed(() => useGlobalStore.modelDialog)
|
||||
const { isMobile } = useBasicLayout()
|
||||
const loginDialog = computed(() => authStore.loginDialog)
|
||||
const globalConfigLoading = computed(() => authStore.globalConfigLoading)
|
||||
const theme = computed(() => appStore.theme)
|
||||
const bgColor = computed(() => theme.value === 'dark' ? '#24272e' : '#fff')
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
const wechatSilentLoginStatus = computed(() => Number(authStore.globalConfig?.wechatSilentLoginStatus) === 1)
|
||||
const homePath = computed(() => authStore.globalConfig?.clientHomePath)
|
||||
|
||||
/* 如果在vx环境并且携带了code则静默登录 */
|
||||
|
||||
function handleCheckOtherLoginByToken() {
|
||||
const { token } = route.query
|
||||
if (token) {
|
||||
authStore.setToken(token)
|
||||
const name = route.name
|
||||
router.replace({ name, query: {} })
|
||||
ms.success('账户登录成功、开始体验吧!')
|
||||
authStore.getUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
/* 微信环境静默登录 */
|
||||
async function loginByWechat() {
|
||||
if (homePath.value || !wechatSilentLoginStatus.value)
|
||||
return
|
||||
if (isLogin.value)
|
||||
return
|
||||
|
||||
/* 如果在vx环境并且携带了code则静默登录 */
|
||||
const { code } = route.query
|
||||
|
||||
if (code) {
|
||||
const res: ResData = await fetchLoginByCodeAPI({ code: code as string })
|
||||
if (res.success) {
|
||||
authStore.setToken(res.data)
|
||||
authStore.getUserInfo()
|
||||
}
|
||||
}
|
||||
else {
|
||||
const url = window.location.href.replace(/#.*$/, '')
|
||||
const res: ResData = await fetchWxLoginRedirectAPI({ url })
|
||||
if (res.success)
|
||||
window.location.href = res.data
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
if (ua.match(/MicroMessenger/i) && ua?.match(/MicroMessenger/i)?.[0] === 'micromessenger')
|
||||
loginByWechat()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
handleCheckOtherLoginByToken()
|
||||
})
|
||||
|
||||
const getMobileMainClass = computed(() => {
|
||||
if (isMobile.value)
|
||||
return ['rounded-none', 'shadow-none']
|
||||
return ['dark:border-neutral-800']
|
||||
})
|
||||
|
||||
const getMobileLayoutClass = computed(() => {
|
||||
if (isMobile.value)
|
||||
return ['flex-col']
|
||||
return ['dark:border-neutral-800']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full dark:bg-[#24272e] transition-all p-0">
|
||||
<div class="h-full overflow-hidden">
|
||||
<div class="z-40 transition flex h-full relative" :class="getMobileLayoutClass">
|
||||
<SiderBar v-if="!isMobile" />
|
||||
<NLayoutContent class="h-full" style="flex: 1" :class="getMobileMainClass">
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</RouterView>
|
||||
</NLayoutContent>
|
||||
<FooterBar v-if="isMobile" />
|
||||
<Loading v-if="globalConfigLoading" :bg-color="bgColor" />
|
||||
</div>
|
||||
<Login :visible="loginDialog" />
|
||||
<PayDialog :visible="payDialog" />
|
||||
<GoodsDialog :visible="goodsDialog" />
|
||||
<NoticeDialog :visible="noticeDialog" />
|
||||
<BindWxDialog :visible="bindWxDialog" />
|
||||
<SignInDialog :visible="signInDialog" />
|
||||
<ModelDialog :visible="modelDialog" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
25
chat/src/layout/siderBar/Logo.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAuthStore } from '@/store'
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const logoPath = computed(() => authStore.globalConfig.clientLogoPath)
|
||||
const homePage = computed(() => authStore.globalConfig.clientHomePath || '/')
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const getMobileClass = computed(() => {
|
||||
if (isMobile.value)
|
||||
return ['py-2', 'w-8', 'ml-3']
|
||||
return ['py-4', 'px-2', 'w-full', 'border-b']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink :to="homePage">
|
||||
<img v-if="!logoPath" src="/logo.png" :class="getMobileClass" class="cursor-pointer px-0 dark:border-[#ffffff17] border-#ebebeb-400" alt="">
|
||||
<img v-if="logoPath" :src="logoPath" :class="getMobileClass" class="cursor-pointer px-0 dark:border-[#ffffff17] border-#ebebeb-400" alt="">
|
||||
</RouterLink>
|
||||
</template>
|
||||
343
chat/src/layout/siderBar/index.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<script setup lang='ts'>
|
||||
import {
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { NAvatar, NIcon, NScrollbar, NTooltip, useMessage } from 'naive-ui'
|
||||
import { PersonAddOutline, PersonRemoveOutline } from '@vicons/ionicons5'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Logo from './Logo.vue'
|
||||
import { HoverButton, SvgIcon } from '@/components/common'
|
||||
import defaultAvatar from '@/assets/avatar.png'
|
||||
import macTablebar from '@/components/base/macTablebar.vue'
|
||||
import { fetchQueryMenuAPI } from '@/api/config'
|
||||
|
||||
import { useAppStore, useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
const Setting = defineAsyncComponent(
|
||||
() => import('@/components/common/Setting/index.vue'),
|
||||
)
|
||||
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const message = useMessage()
|
||||
const track = ref(null)
|
||||
appStore.setEnv()
|
||||
|
||||
const avatar = computed(() => authStore.userInfo.avatar)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const show = ref(false)
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
const darkMode = computed(() => appStore.theme === 'dark')
|
||||
const env = computed(() => appStore.env)
|
||||
const logInIcon = shallowRef(PersonAddOutline)
|
||||
const logOutIcon = shallowRef(PersonRemoveOutline)
|
||||
|
||||
async function queryMenu() {
|
||||
const res: any = await fetchQueryMenuAPI({ menuPlatform: 1 })
|
||||
if (!res.success)
|
||||
return
|
||||
menuList.value = res.data
|
||||
nextTick(() => {
|
||||
calcExceededTotalWidth()
|
||||
})
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
menuName: string
|
||||
menuPath: string
|
||||
menuIcon: string
|
||||
menuTipText: string
|
||||
menuIframeUrl: string
|
||||
isJump: boolean
|
||||
isNeedAuth: boolean
|
||||
}
|
||||
|
||||
const menuList = ref<MenuItem[]>([])
|
||||
const isNeedScroll = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
queryMenu()
|
||||
})
|
||||
|
||||
const signInStatus = computed(
|
||||
() => Number(authStore.globalConfig?.signInStatus) === 1,
|
||||
)
|
||||
|
||||
function toggleLogin() {
|
||||
if (isLogin.value)
|
||||
authStore.logOut()
|
||||
else authStore.setLoginDialog(true)
|
||||
}
|
||||
|
||||
function checkMode() {
|
||||
const mode = darkMode.value ? 'light' : 'dark'
|
||||
appStore.setTheme(mode)
|
||||
}
|
||||
|
||||
function setting() {
|
||||
if (!isLogin.value)
|
||||
authStore.setLoginDialog(true)
|
||||
else show.value = true
|
||||
}
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const activeRoutePath = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
function toPath(name: string) {
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
const mobileSafeArea = computed(() => {
|
||||
if (isMobile.value) {
|
||||
return {
|
||||
paddingBottom: 'env(safe-area-inset-bottom)',
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const getMobileLayoutClass = computed(() => {
|
||||
if (isMobile.value)
|
||||
return ['flex-rol', 'w-full', 'border-0']
|
||||
return ['flex-col', 'w-sider', 'h-full', 'border-r']
|
||||
})
|
||||
|
||||
const getIconMobileLayoutClass = computed(() => {
|
||||
if (isMobile.value)
|
||||
return ['flex', 'flex-rol', 'items-center', 'pt-0', 'w-full']
|
||||
return ['flex', 'flex-col', 'pt-1', 'items-center']
|
||||
})
|
||||
|
||||
const iframeSrc = computed(() => useGlobalStore.iframeUrl)
|
||||
|
||||
function handleClickMenu(menu: MenuItem) {
|
||||
const { menuPath, isJump, menuIframeUrl, isNeedAuth } = menu
|
||||
if (isNeedAuth && !isLogin.value) {
|
||||
message.warning('请先登录后访问!')
|
||||
authStore.setLoginDialog(true)
|
||||
return
|
||||
}
|
||||
useGlobalStore.updateIframeUrl('')
|
||||
if (menuPath) {
|
||||
return router.push({ path: menuPath })
|
||||
}
|
||||
else {
|
||||
if (isJump) {
|
||||
window.open(menuIframeUrl)
|
||||
}
|
||||
else {
|
||||
useGlobalStore.updateIframeUrl(menuIframeUrl)
|
||||
router.push({ path: '/extend' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSignIn() {
|
||||
if (!isLogin.value) {
|
||||
authStore.setLoginDialog(true)
|
||||
return
|
||||
}
|
||||
useGlobalStore.updateSignInDialog(true)
|
||||
}
|
||||
|
||||
function calcExceededTotalWidth() {
|
||||
if (!track.value)
|
||||
return
|
||||
const { clientHeight = 0, scrollHeight = 0 } = track.value
|
||||
isNeedScroll.value = scrollHeight > clientHeight
|
||||
}
|
||||
|
||||
function isActive(item: MenuItem) {
|
||||
const { menuIframeUrl, menuPath } = item
|
||||
if (menuIframeUrl)
|
||||
return menuIframeUrl === iframeSrc.value
|
||||
|
||||
if (menuPath)
|
||||
return menuPath === activeRoutePath.value
|
||||
}
|
||||
|
||||
watch(
|
||||
isMobile,
|
||||
(val) => {
|
||||
appStore.setSiderCollapsed(val)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
flush: 'post',
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex min-w-[80px] bg-[#e8eaf1] pb-2 dark:bg-[#25272d] dark:border-[#3a3a40] border-#efeff5-800"
|
||||
:class="getMobileLayoutClass"
|
||||
:style="mobileSafeArea"
|
||||
>
|
||||
<macTablebar v-if="env === 'electron'" />
|
||||
<div class="px-2 w-full ele-drag">
|
||||
<Logo />
|
||||
</div>
|
||||
<main
|
||||
ref="track"
|
||||
class="flex-1 flex-grow-1 mb-5 overflow-auto"
|
||||
:class="[getIconMobileLayoutClass]"
|
||||
>
|
||||
<NScrollbar :size="1">
|
||||
<div class="flex h-full flex-col items-center space-y-3">
|
||||
<div
|
||||
v-for="item in menuList"
|
||||
:key="item.menuName"
|
||||
class="flex justify-center flex-col items-center"
|
||||
:class="isMobile ? 'mt-0' : 'mt-3'"
|
||||
@click="handleClickMenu(item)"
|
||||
>
|
||||
<NTooltip
|
||||
v-if="!isMobile && signInStatus"
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
>
|
||||
<template #trigger>
|
||||
<div
|
||||
class="h-12 w-12 cursor-pointer bg-white dark:bg-[#34373c] rounded-lg duration-300 flex justify-center items-center"
|
||||
:class="[
|
||||
isActive(item)
|
||||
? 'borderRadis shadow-[#3076fd] btns'
|
||||
: 'border-transparent',
|
||||
]"
|
||||
>
|
||||
<SvgIcon
|
||||
:icon="item.menuIcon"
|
||||
class="text-2xl transition-all"
|
||||
:class="[
|
||||
isActive(item)
|
||||
? 'text-[#4b9e5f] dark:text-[#86dfba]'
|
||||
: '',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
{{ item.menuTipText }}
|
||||
</NTooltip>
|
||||
|
||||
<!-- 改动2:改左侧菜单 -->
|
||||
<!-- <span
|
||||
class="text-[12px] mt-1 margin-auto whitespace-nowrap overflow-hidden"
|
||||
:class="[
|
||||
isActive(item)
|
||||
? 'text-[#4b9e5f] dark:text-[#86dfba] '
|
||||
: 'text-[#999999]',
|
||||
]"
|
||||
>{{ item.menuTipText }}</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</NScrollbar>
|
||||
</main>
|
||||
|
||||
<!-- <HoverButton tooltip="全局设置" :placement="isMobile ? 'bottom' : 'right'" :class="isMobile ? 'pb-0' : 'pb-1' " @click="setting">
|
||||
<NIcon size="20" color="#555">
|
||||
<SvgIcon class="text-2xl" icon="fluent:dark-theme-24-regular" />
|
||||
</NIcon>
|
||||
</HoverButton> -->
|
||||
<!-- <HoverButton v-if="isLogin" tooltip="个人中心" :placement="isMobile ? 'bottom' : 'right'" :class="isMobile ? 'pb-0' : 'pb-8' " @click="toPath('UserCenter')"> -->
|
||||
<!-- <SvgIcon icon="icon-park-twotone:personal-collection" /> -->
|
||||
<!-- h-[140px] -->
|
||||
<div class="flex flex-col justify-between items-center">
|
||||
<NTooltip
|
||||
v-if="!isMobile && signInStatus"
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
>
|
||||
<template #trigger>
|
||||
<SvgIcon
|
||||
class="text-xl cursor-pointer mb-5"
|
||||
icon="streamline-emojis:wrapped-gift-1"
|
||||
style="color: red"
|
||||
@click="handleSignIn"
|
||||
/>
|
||||
</template>
|
||||
签到奖励
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip v-if="!isMobile" trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<SvgIcon
|
||||
class="text-xl cursor-pointer mb-5"
|
||||
:icon="darkMode ? 'noto-v1:last-quarter-moon-face' : 'twemoji:sun'"
|
||||
@click="checkMode"
|
||||
/>
|
||||
</template>
|
||||
主题切换
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip v-if="isLogin" trigger="hover" placement="right">
|
||||
<template #trigger>
|
||||
<NAvatar
|
||||
:size="42"
|
||||
:src="avatar"
|
||||
round
|
||||
bordered
|
||||
:fallback-src="defaultAvatar"
|
||||
class="cursor-pointer"
|
||||
@click="toPath('UserCenter')"
|
||||
/>
|
||||
</template>
|
||||
个人中心
|
||||
</NTooltip>
|
||||
|
||||
<HoverButton
|
||||
v-if="!isLogin"
|
||||
tooltip="登录账户"
|
||||
:placement="isMobile ? 'bottom' : 'right'"
|
||||
:class="isMobile ? 'mb-0' : 'mb-5'"
|
||||
@click="toggleLogin"
|
||||
>
|
||||
<NIcon size="20" color="#555">
|
||||
<component :is="logInIcon" />
|
||||
</NIcon>
|
||||
</HoverButton>
|
||||
</div>
|
||||
</div>
|
||||
<Setting v-if="show" v-model:visible="show" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
overflow: hidden;
|
||||
width: calc(100% - 5px);
|
||||
}
|
||||
.sidebar:hover {
|
||||
width: 100%;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
overflow: hidden;
|
||||
}
|
||||
.overlay:hover {
|
||||
width: 100%;
|
||||
overflow: overlay;
|
||||
}
|
||||
|
||||
.active_bar {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.btns {
|
||||
box-shadow: 0 5px 16px #16993b;
|
||||
}
|
||||
|
||||
.borderRadis {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
91
chat/src/locales/en-US.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export default {
|
||||
common: {
|
||||
add: 'Add',
|
||||
addSuccess: 'Add Success',
|
||||
edit: 'Edit',
|
||||
editSuccess: 'Edit Success',
|
||||
delete: 'Delete',
|
||||
deleteSuccess: 'Delete Success',
|
||||
save: 'Save',
|
||||
saveSuccess: 'Save Success',
|
||||
reset: 'Reset',
|
||||
action: 'Action',
|
||||
export: 'Export',
|
||||
exportSuccess: 'Export Success',
|
||||
import: 'Import',
|
||||
importSuccess: 'Import Success',
|
||||
clear: 'Clear',
|
||||
clearSuccess: 'Clear Success',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
confirm: 'Confirm',
|
||||
download: 'Download',
|
||||
noData: 'No Data',
|
||||
wrong: 'Something went wrong, please try again later.',
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
verify: 'Verify',
|
||||
unauthorizedTips: 'Unauthorized, please verify first.',
|
||||
},
|
||||
chat: {
|
||||
newChatButton: 'New Chat',
|
||||
placeholder: 'Ask me anything...(Shift + Enter = line break)',
|
||||
placeholderMobile: 'Ask me anything...',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
copyCode: 'Copy Code',
|
||||
clearChat: 'Clear Chat',
|
||||
clearChatConfirm: 'Are you sure to clear this chat?',
|
||||
exportImage: 'Export Image',
|
||||
exportImageConfirm: 'Are you sure to export this chat to png?',
|
||||
exportSuccess: 'Export Success',
|
||||
exportFailed: 'Export Failed',
|
||||
usingContext: 'Context Mode',
|
||||
turnOnContext: 'In the current mode, sending messages will carry previous chat records.',
|
||||
turnOffContext: 'In the current mode, sending messages will not carry previous chat records.',
|
||||
deleteMessage: 'Delete Message',
|
||||
deleteMessageConfirm: 'Are you sure to delete this message?',
|
||||
deleteHistoryConfirm: 'Are you sure to clear this history?',
|
||||
clearHistoryConfirm: 'Are you sure to clear chat history?',
|
||||
preview: 'Preview',
|
||||
showRawText: 'Show as raw text',
|
||||
},
|
||||
setting: {
|
||||
setting: 'Setting',
|
||||
general: 'General',
|
||||
advanced: 'Advanced',
|
||||
config: 'Config',
|
||||
avatarLink: 'Avatar Link',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
role: 'Role',
|
||||
resetUserInfo: 'Reset UserInfo',
|
||||
chatHistory: 'ChatHistory',
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
api: 'API',
|
||||
reverseProxy: 'Reverse Proxy',
|
||||
timeout: 'Timeout',
|
||||
socks: 'Socks',
|
||||
httpsProxy: 'HTTPS Proxy',
|
||||
balance: 'API Balance',
|
||||
},
|
||||
store: {
|
||||
siderButton: 'Prompt Store',
|
||||
local: 'Local',
|
||||
online: 'Online',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
clearStoreConfirm: 'Whether to clear the data?',
|
||||
importPlaceholder: 'Please paste the JSON data here',
|
||||
addRepeatTitleTips: 'Title duplicate, please re-enter',
|
||||
addRepeatContentTips: 'Content duplicate: {msg}, please re-enter',
|
||||
editRepeatTitleTips: 'Title conflict, please revise',
|
||||
editRepeatContentTips: 'Content conflict {msg} , please re-modify',
|
||||
importError: 'Key value mismatch',
|
||||
importRepeatTitle: 'Title repeatedly skipped: {msg}',
|
||||
importRepeatContent: 'Content is repeatedly skipped: {msg}',
|
||||
onlineImportWarning: 'Note: Please check the JSON file source!',
|
||||
downloadError: 'Please check the network status and JSON file validity',
|
||||
},
|
||||
}
|
||||
34
chat/src/locales/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { App } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import enUS from './en-US'
|
||||
import zhCN from './zh-CN'
|
||||
import zhTW from './zh-TW'
|
||||
import { useAppStoreWithOut } from '@/store/modules/app'
|
||||
import type { Language } from '@/store/modules/app/helper'
|
||||
|
||||
const appStore = useAppStoreWithOut()
|
||||
|
||||
const defaultLocale = appStore.language || 'zh-CN'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: defaultLocale,
|
||||
fallbackLocale: 'en-US',
|
||||
allowComposition: true,
|
||||
messages: {
|
||||
'en-US': enUS,
|
||||
'zh-CN': zhCN,
|
||||
'zh-TW': zhTW,
|
||||
},
|
||||
})
|
||||
|
||||
export const t = i18n.global.t
|
||||
|
||||
export function setLocale(locale: Language) {
|
||||
i18n.global.locale = locale
|
||||
}
|
||||
|
||||
export function setupI18n(app: App) {
|
||||
app.use(i18n)
|
||||
}
|
||||
|
||||
export default i18n
|
||||
95
chat/src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export default {
|
||||
common: {
|
||||
add: '添加',
|
||||
addSuccess: '添加成功',
|
||||
edit: '编辑',
|
||||
editSuccess: '编辑成功',
|
||||
delete: '删除',
|
||||
deleteSuccess: '删除成功',
|
||||
// save: '保存',
|
||||
update: '修改',
|
||||
saveSuccess: '保存成功',
|
||||
updateUserSuccess: '修改用户信息成功',
|
||||
reset: '重置',
|
||||
action: '操作',
|
||||
export: '导出',
|
||||
exportSuccess: '导出成功',
|
||||
import: '导入',
|
||||
importSuccess: '导入成功',
|
||||
clear: '清空',
|
||||
clearSuccess: '清空成功',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
confirm: '确定',
|
||||
download: '下载',
|
||||
noData: '暂无数据',
|
||||
wrong: '好像出错了,请稍后再试。',
|
||||
success: '操作成功',
|
||||
failed: '操作失败',
|
||||
verify: '验证',
|
||||
unauthorizedTips: '未经授权,请先进行验证。',
|
||||
},
|
||||
chat: {
|
||||
newChatButton: '新建聊天',
|
||||
placeholder: '来说点什么吧...(Shift + Enter = 换行)',
|
||||
placeholderMobile: '来说点什么...',
|
||||
copy: '复制',
|
||||
copied: '复制成功',
|
||||
copyCode: '复制代码',
|
||||
clearChat: '清空会话',
|
||||
clearChatConfirm: '是否清空会话?',
|
||||
exportImage: '保存会话到图片',
|
||||
exportImageConfirm: '是否将会话保存为图片?',
|
||||
exportSuccess: '保存成功',
|
||||
exportFailed: '保存失败',
|
||||
usingContext: '上下文模式',
|
||||
turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录',
|
||||
turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录',
|
||||
deleteMessage: '删除消息',
|
||||
deleteMessageConfirm: '是否删除此消息?',
|
||||
deleteHistoryConfirm: '确定删除此记录?',
|
||||
clearHistoryConfirm: '确定清空聊天记录?',
|
||||
preview: '预览',
|
||||
showRawText: '显示原文',
|
||||
},
|
||||
setting: {
|
||||
setting: '设置',
|
||||
general: '总览',
|
||||
advanced: '高级',
|
||||
// config: '配置',
|
||||
personalInfo: '个人信息',
|
||||
avatarLink: '头像链接',
|
||||
// name: '名称',
|
||||
name: '用户名称',
|
||||
sign: '用户签名',
|
||||
role: '角色设定',
|
||||
resetUserInfo: '重置用户信息',
|
||||
chatHistory: '聊天记录',
|
||||
theme: '主题',
|
||||
language: '语言',
|
||||
api: 'API',
|
||||
reverseProxy: '反向代理',
|
||||
timeout: '超时',
|
||||
socks: 'Socks',
|
||||
httpsProxy: 'HTTPS Proxy',
|
||||
balance: 'API余额',
|
||||
},
|
||||
store: {
|
||||
siderButton: '提示词商店',
|
||||
local: '本地',
|
||||
online: '在线',
|
||||
title: '标题',
|
||||
description: '描述',
|
||||
clearStoreConfirm: '是否清空数据?',
|
||||
importPlaceholder: '请粘贴 JSON 数据到此处',
|
||||
addRepeatTitleTips: '标题重复,请重新输入',
|
||||
addRepeatContentTips: '内容重复:{msg},请重新输入',
|
||||
editRepeatTitleTips: '标题冲突,请重新修改',
|
||||
editRepeatContentTips: '内容冲突{msg} ,请重新修改',
|
||||
importError: '键值不匹配',
|
||||
importRepeatTitle: '标题重复跳过:{msg}',
|
||||
importRepeatContent: '内容重复跳过:{msg}',
|
||||
onlineImportWarning: '注意:请检查 JSON 文件来源!',
|
||||
downloadError: '请检查网络状态与 JSON 文件有效性',
|
||||
},
|
||||
}
|
||||
91
chat/src/locales/zh-TW.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export default {
|
||||
common: {
|
||||
add: '新增',
|
||||
addSuccess: '新增成功',
|
||||
edit: '編輯',
|
||||
editSuccess: '編輯成功',
|
||||
delete: '刪除',
|
||||
deleteSuccess: '刪除成功',
|
||||
save: '儲存',
|
||||
saveSuccess: '儲存成功',
|
||||
reset: '重設',
|
||||
action: '操作',
|
||||
export: '匯出',
|
||||
exportSuccess: '匯出成功',
|
||||
import: '匯入',
|
||||
importSuccess: '匯入成功',
|
||||
clear: '清除',
|
||||
clearSuccess: '清除成功',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
confirm: '確認',
|
||||
download: '下載',
|
||||
noData: '目前無資料',
|
||||
wrong: '發生錯誤,請稍後再試。',
|
||||
success: '操作成功',
|
||||
failed: '操作失敗',
|
||||
verify: '驗證',
|
||||
unauthorizedTips: '未經授權,請先進行驗證。',
|
||||
},
|
||||
chat: {
|
||||
newChatButton: '新建對話',
|
||||
placeholder: '來說點什麼...(Shift + Enter = 換行)',
|
||||
placeholderMobile: '來說點什麼...',
|
||||
copy: '複製',
|
||||
copied: '複製成功',
|
||||
copyCode: '複製代碼',
|
||||
clearChat: '清除對話',
|
||||
clearChatConfirm: '是否清空對話?',
|
||||
exportImage: '儲存對話為圖片',
|
||||
exportImageConfirm: '是否將對話儲存為圖片?',
|
||||
exportSuccess: '儲存成功',
|
||||
exportFailed: '儲存失敗',
|
||||
usingContext: '上下文模式',
|
||||
turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。',
|
||||
turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。',
|
||||
deleteMessage: '刪除訊息',
|
||||
deleteMessageConfirm: '是否刪除此訊息?',
|
||||
deleteHistoryConfirm: '確定刪除此紀錄?',
|
||||
clearHistoryConfirm: '確定清除紀錄?',
|
||||
preview: '預覽',
|
||||
showRawText: '顯示原文',
|
||||
},
|
||||
setting: {
|
||||
setting: '設定',
|
||||
general: '總覽',
|
||||
advanced: '高級',
|
||||
config: '設定',
|
||||
avatarLink: '頭貼連結',
|
||||
name: '名稱',
|
||||
description: '描述',
|
||||
role: '角色設定',
|
||||
resetUserInfo: '重設使用者資訊',
|
||||
chatHistory: '紀錄',
|
||||
theme: '主題',
|
||||
language: '語言',
|
||||
api: 'API',
|
||||
reverseProxy: '反向代理',
|
||||
timeout: '逾時',
|
||||
socks: 'Socks',
|
||||
httpsProxy: 'HTTPS Proxy',
|
||||
balance: 'API余額',
|
||||
},
|
||||
store: {
|
||||
siderButton: '提示詞商店',
|
||||
local: '本機',
|
||||
online: '線上',
|
||||
title: '標題',
|
||||
description: '描述',
|
||||
clearStoreConfirm: '是否清除資料?',
|
||||
importPlaceholder: '請將 JSON 資料貼在此處',
|
||||
addRepeatTitleTips: '標題重複,請重新輸入',
|
||||
addRepeatContentTips: '內容重複:{msg},請重新輸入',
|
||||
editRepeatTitleTips: '標題衝突,請重新修改',
|
||||
editRepeatContentTips: '內容衝突{msg} ,請重新修改',
|
||||
importError: '鍵值不符合',
|
||||
importRepeatTitle: '因標題重複跳過:{msg}',
|
||||
importRepeatContent: '因內容重複跳過:{msg}',
|
||||
onlineImportWarning: '注意:請檢查 JSON 檔案來源!',
|
||||
downloadError: '請檢查網路狀態與 JSON 檔案有效性',
|
||||
},
|
||||
}
|
||||
29
chat/src/main.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createApp } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import '@/styles/lib/viewer.css'
|
||||
import VueViewer from 'v-viewer'
|
||||
import App from './App.vue'
|
||||
import { setupI18n } from './locales'
|
||||
import { setupAssets, setupScrollbarStyle } from './plugins'
|
||||
import { setupStore } from './store'
|
||||
import { setupRouter } from './router'
|
||||
import { MotionPlugin } from "@vueuse/motion";
|
||||
|
||||
import '@/styles/transition.less'
|
||||
import '@/styles/notice.less'
|
||||
|
||||
window.$message = useMessage()
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App)
|
||||
app.use(VueViewer)
|
||||
app.use(MotionPlugin)
|
||||
setupAssets()
|
||||
setupScrollbarStyle()
|
||||
setupStore(app)
|
||||
setupI18n(app)
|
||||
await setupRouter(app)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
18
chat/src/plugins/assets.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'katex/dist/katex.min.css'
|
||||
import '@/styles/lib/tailwind.css'
|
||||
import '@/styles/lib/highlight.less'
|
||||
import '@/styles/lib/github-markdown.less'
|
||||
import '@/styles/global.less'
|
||||
|
||||
/** Tailwind's Preflight Style Override */
|
||||
function naiveStyleOverride() {
|
||||
const meta = document.createElement('meta')
|
||||
meta.name = 'naive-ui-style'
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
|
||||
function setupAssets() {
|
||||
naiveStyleOverride()
|
||||
}
|
||||
|
||||
export default setupAssets
|
||||
4
chat/src/plugins/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import setupAssets from './assets'
|
||||
import setupScrollbarStyle from './scrollbarStyle'
|
||||
|
||||
export { setupAssets, setupScrollbarStyle }
|
||||
28
chat/src/plugins/scrollbarStyle.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { darkTheme, lightTheme } from 'naive-ui'
|
||||
|
||||
const setupScrollbarStyle = () => {
|
||||
const style = document.createElement('style')
|
||||
const styleContent = `
|
||||
::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
width: ${lightTheme.Scrollbar.common?.scrollbarWidth};
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: ${lightTheme.Scrollbar.common?.scrollbarColor};
|
||||
border-radius: ${lightTheme.Scrollbar.common?.scrollbarBorderRadius};
|
||||
}
|
||||
html.dark ::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
width: ${darkTheme.Scrollbar.common?.scrollbarWidth};
|
||||
}
|
||||
html.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: ${darkTheme.Scrollbar.common?.scrollbarColor};
|
||||
border-radius: ${darkTheme.Scrollbar.common?.scrollbarBorderRadius};
|
||||
}
|
||||
`
|
||||
|
||||
style.innerHTML = styleContent
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
export default setupScrollbarStyle
|
||||
110
chat/src/router/index.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { App } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { setupPageGuard } from './permission'
|
||||
import { Layout } from '@/layout'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: Layout,
|
||||
redirect: '/chat',
|
||||
children: [
|
||||
{
|
||||
path: '/market',
|
||||
name: 'Market',
|
||||
component: () => import('@/views/market/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/draw',
|
||||
name: 'Draw',
|
||||
component: () => import('@/views/draw/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/midjourney',
|
||||
name: 'Midjourney',
|
||||
component: () => import('@/views/midjourney/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
name: 'Chat',
|
||||
component: () => import('@/views/chat/chat.vue'),
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'Role',
|
||||
component: () => import('@/views/chat/role.vue'),
|
||||
},
|
||||
{
|
||||
path: 'user-center',
|
||||
name: 'UserCenter',
|
||||
component: () => import('@/views/userCenter/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'app-store',
|
||||
name: 'AppStore',
|
||||
component: () => import('@/views/appStore/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'pay',
|
||||
name: 'Pay',
|
||||
component: () => import('@/views/pay/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'mind',
|
||||
name: 'Mind',
|
||||
component: () => import('@/views/mind/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'share',
|
||||
name: 'Share',
|
||||
component: () => import('@/views/share/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'extend',
|
||||
name: 'Extend',
|
||||
component: () => import('@/views/extend/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'inpaint',
|
||||
name: 'Inpaint',
|
||||
component: () => import('@/views/inpaint/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/welcome',
|
||||
name: 'Welcome',
|
||||
component: () => import('@/views/welcome/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: '404',
|
||||
component: () => import('@/views/exception/404/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: '500',
|
||||
component: () => import('@/views/exception/500/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notFound',
|
||||
redirect: '/404',
|
||||
},
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
// history: createWebHashHistory(),
|
||||
routes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
setupPageGuard(router)
|
||||
|
||||
export async function setupRouter(app: App) {
|
||||
app.use(router)
|
||||
await router.isReady()
|
||||
}
|
||||
60
chat/src/router/permission.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Router } from 'vue-router'
|
||||
import { useAuthStoreWithout } from '@/store/modules/auth'
|
||||
import { ss } from '@/utils/storage'
|
||||
import { fetchInviteCodeAPI } from '@/api/user'
|
||||
|
||||
export function setupPageGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const { inVitecode } = to.query
|
||||
inVitecode && ss.set('invitedBy', inVitecode as string)
|
||||
if (inVitecode) {
|
||||
await fetchInviteCodeAPI({ code: inVitecode })
|
||||
router.replace({ path: to.path, query: {} })
|
||||
}
|
||||
|
||||
window.$loadingBar?.start()
|
||||
const authStore = useAuthStoreWithout()
|
||||
if (!authStore.userInfo.username) {
|
||||
try {
|
||||
authStore.token && await authStore.getUserInfo()
|
||||
if (authStore.globalConfigLoading) {
|
||||
let domain = `${window.location.protocol}//${window.location.hostname}`
|
||||
if (window.location.port)
|
||||
domain += `:${window.location.port}`
|
||||
await authStore.getglobalConfig(domain)
|
||||
if (authStore.globalConfig.clientHomePath)
|
||||
next({ path: authStore.globalConfig.clientHomePath })
|
||||
|
||||
else
|
||||
next()
|
||||
}
|
||||
if (to.path === '/500')
|
||||
next({ path: '/' })
|
||||
else
|
||||
next()
|
||||
}
|
||||
catch (error) {
|
||||
if (to.path === '/500')
|
||||
next({ path: '/' })
|
||||
else
|
||||
next()
|
||||
}
|
||||
}
|
||||
else {
|
||||
const clientMenuList = authStore.globalConfig?.clientMenuList
|
||||
const openMenuList = clientMenuList ? JSON.parse(clientMenuList) : []
|
||||
if (openMenuList.length && !openMenuList.includes(to.name) && ['Chat', 'Draw', 'Midjourney'].includes(to.name)) {
|
||||
if (authStore.globalConfig.clientHomePath && authStore.globalConfig.clientHomePath !== '')
|
||||
next({ path: authStore.globalConfig.clientHomePath })
|
||||
|
||||
else next()
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to: any) => {
|
||||
window.$loadingBar?.finish()
|
||||
})
|
||||
}
|
||||
10
chat/src/store/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export const store = createPinia()
|
||||
|
||||
export function setupStore(app: App) {
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
export * from './modules'
|
||||
46
chat/src/store/modules/app/helper.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
function detectEnvironment() {
|
||||
if (typeof process !== 'undefined' && process?.type === 'renderer')
|
||||
return 'electron'
|
||||
|
||||
else if (typeof wx !== 'undefined')
|
||||
return 'wechat'
|
||||
|
||||
else if (typeof window !== 'undefined' && window.matchMedia('(display-mode: standalone)').matches)
|
||||
return 'webApp'
|
||||
|
||||
else if (/(Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone)/i.test(navigator.userAgent))
|
||||
return 'mobile'
|
||||
|
||||
else
|
||||
return 'webBrowser'
|
||||
}
|
||||
|
||||
const LOCAL_NAME = 'appSetting'
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'auto'
|
||||
|
||||
export type Language = 'zh-CN' | 'zh-TW' | 'en-US'
|
||||
|
||||
export type Env = 'electron' | 'wecaht' | 'web' | 'mobile'
|
||||
|
||||
export interface AppState {
|
||||
siderCollapsed: boolean
|
||||
theme: Theme
|
||||
language: Language
|
||||
env: Env
|
||||
}
|
||||
|
||||
export function defaultSetting(): AppState {
|
||||
return { siderCollapsed: false, theme: 'auto', language: 'zh-CN', env: detectEnvironment() }
|
||||
}
|
||||
|
||||
export function getLocalSetting(): AppState {
|
||||
const localSetting: AppState | undefined = ss.get(LOCAL_NAME)
|
||||
return { ...defaultSetting(), ...localSetting }
|
||||
}
|
||||
|
||||
export function setLocalSetting(setting: AppState): void {
|
||||
ss.set(LOCAL_NAME, setting)
|
||||
}
|
||||
58
chat/src/store/modules/app/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AppState, Language, Theme } from './helper'
|
||||
import { getLocalSetting, setLocalSetting } from './helper'
|
||||
import { store } from '@/store'
|
||||
|
||||
export const useAppStore = defineStore('app-store', {
|
||||
state: (): AppState => getLocalSetting(),
|
||||
actions: {
|
||||
setSiderCollapsed(collapsed: boolean) {
|
||||
this.siderCollapsed = collapsed
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
setTheme(theme: Theme) {
|
||||
localStorage.theme = theme
|
||||
this.theme = theme
|
||||
window.theme = theme
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
setLanguage(language: Language) {
|
||||
if (this.language !== language) {
|
||||
this.language = language
|
||||
this.recordState()
|
||||
}
|
||||
},
|
||||
|
||||
recordState() {
|
||||
setLocalSetting(this.$state)
|
||||
},
|
||||
|
||||
setEnv() {
|
||||
const isWeChat = /micromessenger/i.test(navigator.userAgent)
|
||||
|
||||
const isElectron = navigator.userAgent.includes('Electron')
|
||||
|
||||
const isMobile = /(iPhone|iPad|iPod|Android|webOS|BlackBerry|Windows Phone)/i.test(navigator.userAgent)
|
||||
|
||||
const isWeb = !isWeChat && !isElectron
|
||||
|
||||
if (isWeChat)
|
||||
this.env = 'wechat'
|
||||
|
||||
else if (isElectron)
|
||||
this.env = 'electron'
|
||||
|
||||
else if (isMobile)
|
||||
this.env = 'mobile'
|
||||
|
||||
else if (isWeb)
|
||||
this.env = 'web'
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function useAppStoreWithOut() {
|
||||
return useAppStore(store)
|
||||
}
|
||||
20
chat/src/store/modules/appStore/helper.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface MineApp {
|
||||
userId: number
|
||||
catId: number
|
||||
appId: number
|
||||
public: boolean
|
||||
status: number
|
||||
demoData: string
|
||||
order: number
|
||||
appDes: string
|
||||
preset: string
|
||||
appRole: string
|
||||
coverImg: string
|
||||
appName: string
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export interface AppStoreState {
|
||||
catId: number
|
||||
mineApps: MineApp[]
|
||||
}
|
||||
26
chat/src/store/modules/appStore/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AppStoreState } from './helper'
|
||||
import { fetchQueryMineAppsAPI } from '@/api/appStore'
|
||||
import { store } from '@/store'
|
||||
|
||||
export const useAppCatStore = defineStore('app-cat-store', {
|
||||
state: (): AppStoreState => ({
|
||||
catId: 0,
|
||||
mineApps: [],
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setCatId(catId: number) {
|
||||
this.catId = catId
|
||||
},
|
||||
|
||||
async queryMineApps() {
|
||||
const res = await fetchQueryMineAppsAPI()
|
||||
this.mineApps = res?.data?.rows || []
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function useAppCatStoreWithOut() {
|
||||
return useAppStore(store)
|
||||
}
|
||||
110
chat/src/store/modules/auth/helper.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'SECRET_TOKEN'
|
||||
|
||||
export function getToken() {
|
||||
return ss.get(LOCAL_NAME)
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
return ss.set(LOCAL_NAME, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return ss.remove(LOCAL_NAME)
|
||||
}
|
||||
|
||||
export interface UserBalance {
|
||||
isMember: boolean
|
||||
model3Count: number
|
||||
model4Count: number
|
||||
drawMjCount: number
|
||||
memberModel3Count: number
|
||||
memberModel4Count: number
|
||||
memberDrawMjCount: number
|
||||
useModel3Count: number
|
||||
useModel4Count: number
|
||||
useModel3Token: number
|
||||
useModel4Token: number
|
||||
useDrawMjToken: number
|
||||
sumModel3Count: number
|
||||
sumModel4Count: number
|
||||
sumDrawMjCount: number
|
||||
expirationTime: Date
|
||||
}
|
||||
|
||||
export interface GlobalConfig {
|
||||
siteName: string
|
||||
qqNumber: string
|
||||
vxNumber: string
|
||||
baiduCode: string
|
||||
buyCramiAddress: string
|
||||
noticeInfo: string
|
||||
inviteSendStatus: string
|
||||
registerSendStatus: string
|
||||
registerSendModel3Count: string
|
||||
registerSendModel4Count: string
|
||||
registerSendDrawMjCount: string
|
||||
inviteGiveSendModel3Count: string
|
||||
inviteGiveSendModel4Count: string
|
||||
inviteGiveSendDrawMjCount: string
|
||||
invitedGuestSendModel3Count: string
|
||||
invitedGuestSendModel4Count: string
|
||||
invitedGuestSendDrawMjCount: string
|
||||
clientHomePath: string
|
||||
clientLogoPath: string
|
||||
clientFavoIconPath: string
|
||||
isUseWxLogin: boolean
|
||||
robotAvatar: string
|
||||
siteRobotName: string
|
||||
mindDefaultData: string
|
||||
payEpayStatus: string
|
||||
payHupiStatus: string
|
||||
payWechatStatus: string
|
||||
payEpayChannel: string
|
||||
payMpayChannel: string
|
||||
payEpayApiPayUrl: string
|
||||
payMpayStatus: string
|
||||
isAutoOpenNotice: string
|
||||
isShowAppCatIcon: string
|
||||
salesBaseRatio: string
|
||||
salesSeniorRatio: string
|
||||
salesAllowDrawMoney: string
|
||||
companyName: string
|
||||
filingNumber: string
|
||||
emailRegisterStatus: string
|
||||
emailLoginStatus: string
|
||||
phoneLoginStatus: string
|
||||
phoneRegisterStatus: string
|
||||
wechatRegisterStatus: string
|
||||
wechatSilentLoginStatus: string
|
||||
signInStatus: string
|
||||
signInModel3Count: string
|
||||
signInModel4Count: string
|
||||
signInMjDrawToken: string
|
||||
appMenuHeaderTips: string
|
||||
appMenuHeaderBgUrl: string
|
||||
mjHideNotBlock: string
|
||||
mjUseBaiduFy: string
|
||||
mjHideWorkIn: string
|
||||
isVerifyEmail: string
|
||||
}
|
||||
export interface AuthState {
|
||||
token: string | undefined
|
||||
loginDialog: boolean
|
||||
globalConfigLoading: boolean
|
||||
loadInit: boolean
|
||||
userInfo: {
|
||||
username: string
|
||||
email: string
|
||||
role: string
|
||||
id: number
|
||||
avatar?: string
|
||||
sign?: string
|
||||
inviteCode: string
|
||||
isBindWx: boolean
|
||||
consecutiveDays: number
|
||||
}
|
||||
userBalance: UserBalance
|
||||
globalConfig: GlobalConfig
|
||||
}
|
||||
101
chat/src/store/modules/auth/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useChatStore } from '../chat'
|
||||
|
||||
import type { AuthState, GlobalConfig, UserBalance } from './helper'
|
||||
import { getToken, removeToken, setToken } from './helper'
|
||||
import { store } from '@/store'
|
||||
import { fetchGetInfo } from '@/api'
|
||||
import { fetchQueryConfigAPI } from '@/api/config'
|
||||
import { fetchGetBalanceQueryAPI } from '@/api/balance'
|
||||
import type { ResData } from '@/api/types'
|
||||
|
||||
export const useAuthStore = defineStore('auth-store', {
|
||||
state: (): AuthState => ({
|
||||
token: getToken(),
|
||||
loginDialog: false,
|
||||
globalConfigLoading: true,
|
||||
userInfo: {},
|
||||
userBalance: {},
|
||||
globalConfig: {} as GlobalConfig,
|
||||
loadInit: false,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isLogin: (state: AuthState) => !!state.token,
|
||||
},
|
||||
|
||||
actions: {
|
||||
async getUserInfo(): Promise<T> {
|
||||
try {
|
||||
if (!this.loadInit)
|
||||
await this.getglobalConfig()
|
||||
|
||||
const res = await fetchGetInfo()
|
||||
if (!res)
|
||||
return Promise.resolve(res)
|
||||
const { data } = res
|
||||
const { userInfo, userBalance } = data
|
||||
this.userInfo = { ...userInfo }
|
||||
this.userBalance = { ...userBalance }
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
|
||||
updateUserBanance(userBalance: UserBalance) {
|
||||
this.userBalance = userBalance
|
||||
},
|
||||
|
||||
async getUserBalance() {
|
||||
const res: ResData = await fetchGetBalanceQueryAPI()
|
||||
const { success, data } = res
|
||||
if (success)
|
||||
this.userBalance = data
|
||||
},
|
||||
|
||||
async getglobalConfig(domain = '') {
|
||||
const res = await fetchQueryConfigAPI({ domain })
|
||||
this.globalConfig = res.data
|
||||
this.globalConfigLoading = false
|
||||
this.loadInit = true
|
||||
},
|
||||
|
||||
setToken(token: string) {
|
||||
this.token = token
|
||||
setToken(token)
|
||||
},
|
||||
|
||||
removeToken() {
|
||||
this.token = undefined
|
||||
removeToken()
|
||||
},
|
||||
|
||||
setLoginDialog(bool: boolean) {
|
||||
this.loginDialog = bool
|
||||
},
|
||||
|
||||
logOut() {
|
||||
this.token = undefined
|
||||
removeToken()
|
||||
this.userInfo = {}
|
||||
this.userBalance = {}
|
||||
window.$message.success('登出账户成功!')
|
||||
const chatStore = useChatStore()
|
||||
chatStore.clearChat()
|
||||
},
|
||||
|
||||
updatePasswordSuccess() {
|
||||
this.token = undefined
|
||||
removeToken()
|
||||
this.userInfo = {}
|
||||
this.userBalance = {}
|
||||
this.loginDialog = true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function useAuthStoreWithout() {
|
||||
return useAuthStore(store)
|
||||
}
|
||||
41
chat/src/store/modules/chat/helper.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'chatStorage'
|
||||
|
||||
export function defaultState(): Chat.ChatState {
|
||||
return {
|
||||
active: 0,
|
||||
usingContext: true,
|
||||
usingNetwork: false,
|
||||
groupList: [],
|
||||
chatList: [],
|
||||
groupKeyWord: '',
|
||||
baseConfig: null
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalState(): Chat.ChatState {
|
||||
const localState = ss.get(LOCAL_NAME)
|
||||
return { ...defaultState(), ...localState }
|
||||
}
|
||||
|
||||
export function setLocalState({ active }: Chat.ChatState) {
|
||||
ss.set(LOCAL_NAME, { ...ss.get(LOCAL_NAME), active })
|
||||
}
|
||||
|
||||
export function formatChatPre(data: any): any{
|
||||
return data.map( (item: any) => {
|
||||
const { name, childList, id } = item
|
||||
return {
|
||||
label: name,
|
||||
value: id,
|
||||
children: childList.map( (t: any) => {
|
||||
return {
|
||||
label: t.title,
|
||||
value: t.prompt
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
215
chat/src/store/modules/chat/index.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { formatChatPre, getLocalState, setLocalState } from './helper'
|
||||
import { fetchCreateGroupAPI, fetchDelAllGroupAPI, fetchDelGroupAPI, fetchQueryGroupAPI, fetchUpdateGroupAPI } from '@/api/group'
|
||||
import { fetchDelChatLogAPI, fetchDelChatLogByGroupIdAPI, fetchQueryChatLogListAPI } from '@/api/chatLog'
|
||||
import { fetchModelBaseConfigAPI } from '@/api/models'
|
||||
import { fetchGetChatPreList } from '@/api/index'
|
||||
|
||||
export const useChatStore = defineStore('chat-store', {
|
||||
state: (): Chat.ChatState => getLocalState(),
|
||||
|
||||
getters: {
|
||||
/* 当前选用模型的配置 */
|
||||
activeConfig: (state) => {
|
||||
const uuid = state.active
|
||||
if (!uuid)
|
||||
return {}
|
||||
const config = state.groupList.find(item => item.uuid === uuid)?.config
|
||||
return config ? JSON.parse(config) : state.baseConfig
|
||||
},
|
||||
|
||||
activeGroupAppId: (state) => {
|
||||
const uuid = state.active
|
||||
if (!uuid)
|
||||
return null
|
||||
return state.groupList.find(item => item.uuid === uuid)?.appId
|
||||
},
|
||||
|
||||
/* 当前选用模型的扣费类型 */
|
||||
activeModelKeyDeductType(state) {
|
||||
return this.activeConfig?.modelInfo?.deductType
|
||||
},
|
||||
|
||||
/* 当前选用模型的模型类型 */
|
||||
activeModelKeyType(state) {
|
||||
return this.activeConfig?.modelInfo?.keyType
|
||||
},
|
||||
|
||||
/* 当前选用模型的调用价格 */
|
||||
activeModelKeyPrice(state) {
|
||||
return this.activeConfig?.modelInfo?.deduct
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
actions: {
|
||||
/* 对话组过滤 */
|
||||
setGroupKeyWord(keyWord: string) {
|
||||
this.groupKeyWord = keyWord
|
||||
},
|
||||
|
||||
/* 计算拿到当前选择的对话组信息 */
|
||||
getChatByGroupInfo() {
|
||||
if (this.active)
|
||||
return this.groupList.find(item => item.uuid === this.active) || {}
|
||||
},
|
||||
|
||||
/* */
|
||||
getConfigFromUuid(uuid: any) {
|
||||
return this.groupList.find(item => item.uuid === uuid)?.config
|
||||
},
|
||||
|
||||
/* 新增新的对话组 */
|
||||
async addNewChatGroup(appId = 0) {
|
||||
const res: any = await fetchCreateGroupAPI({ appId })
|
||||
const { id: uuid } = res.data
|
||||
await this.setActiveGroup(uuid)
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 查询基础模型配置 兼容老的chatgroup */
|
||||
async getBaseModelConfig() {
|
||||
const res = await fetchModelBaseConfigAPI()
|
||||
this.baseConfig = res?.data
|
||||
},
|
||||
|
||||
/* 查询我的对话组 */
|
||||
async queryMyGroup() {
|
||||
const res: any = await fetchQueryGroupAPI()
|
||||
this.groupList = [...res.data.map((item: any) => {
|
||||
const { id: uuid, title, isSticky, createdAt, updatedAt, appId, config, appLogo } = item
|
||||
return { uuid, title, isEdit: false, appId, config, isSticky, appLogo, createdAt, updatedAt: new Date(updatedAt).getTime() }
|
||||
})]
|
||||
const isHasActive = this.groupList.some(item => Number(item.uuid) === Number(this.active))
|
||||
if (!this.active || !isHasActive)
|
||||
this.groupList.length && this.setActiveGroup(this.groupList[0].uuid)
|
||||
},
|
||||
|
||||
/* 修改对话组信息 */
|
||||
async updateGroupInfo(params: { groupId: number; title?: string; isSticky?: boolean }) {
|
||||
await fetchUpdateGroupAPI(params)
|
||||
},
|
||||
|
||||
/* 变更对话组 */
|
||||
async setActiveGroup(uuid: number) {
|
||||
this.active = uuid
|
||||
if (this.active)
|
||||
await this.queryActiveChatLogList()
|
||||
|
||||
else
|
||||
this.chatList = []
|
||||
|
||||
this.groupList.forEach(item => (item.isEdit = false))
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 删除对话组 */
|
||||
async deleteGroup(params: Chat.History) {
|
||||
const curIndex = this.groupList.findIndex(item => item.uuid === params.uuid)
|
||||
const { uuid: groupId } = params
|
||||
await fetchDelGroupAPI({ groupId })
|
||||
await this.queryMyGroup()
|
||||
if (this.groupList.length === 0)
|
||||
await this.setActiveGroup(0)
|
||||
|
||||
if (curIndex > 0 && curIndex < this.groupList.length)
|
||||
await this.setActiveGroup(this.groupList[curIndex].uuid)
|
||||
|
||||
if (curIndex === 0 && this.groupList.length > 0)
|
||||
await this.setActiveGroup(this.groupList[0].uuid)
|
||||
|
||||
if (curIndex > this.groupList.length || (curIndex === 0 && this.groupList.length === 0))
|
||||
await this.setActiveGroup(0)
|
||||
|
||||
if (curIndex > 0 && curIndex === this.groupList.length)
|
||||
await this.setActiveGroup(this.groupList[curIndex - 1].uuid)
|
||||
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 删除全部非置顶对话组 */
|
||||
async delAllGroup() {
|
||||
if (!this.active || !this.groupList.length)
|
||||
return
|
||||
await fetchDelAllGroupAPI()
|
||||
await this.queryMyGroup()
|
||||
if (this.groupList.length === 0)
|
||||
await this.setActiveGroup(0)
|
||||
|
||||
else
|
||||
await this.setActiveGroup(this.groupList[0].uuid)
|
||||
},
|
||||
|
||||
/* 查询当前对话组的聊天记录 */
|
||||
async queryActiveChatLogList() {
|
||||
if (!this.active || Number(this.active) === 0)
|
||||
return
|
||||
const res: any = await fetchQueryChatLogListAPI({ groupId: this.active })
|
||||
this.chatList = res.data
|
||||
},
|
||||
|
||||
/* 添加一条虚拟的对话记录 */
|
||||
addGroupChat(data) {
|
||||
this.chatList = [...this.chatList, data]
|
||||
},
|
||||
|
||||
/* 动态修改对话记录 */
|
||||
updateGroupChat(index: number, data: Chat.Chat) {
|
||||
this.chatList[index] = { ...this.chatList[index], ...data }
|
||||
},
|
||||
|
||||
/* 修改其中部分内容 */
|
||||
updateGroupChatSome(index: number, data: Partial<Chat.Chat>) {
|
||||
this.chatList[index] = { ...this.chatList[index], ...data }
|
||||
},
|
||||
|
||||
/* 删除一条对话记录 */
|
||||
async deleteChatById(chatId: number | undefined) {
|
||||
console.log(chatId)
|
||||
if (!chatId)
|
||||
return
|
||||
await fetchDelChatLogAPI({ id: chatId })
|
||||
await this.queryActiveChatLogList()
|
||||
},
|
||||
|
||||
/* 查询快问预设 */
|
||||
async queryChatPre() {
|
||||
const res: any = await fetchGetChatPreList()
|
||||
if (!res.data)
|
||||
return
|
||||
this.chatPreList = formatChatPre(res.data)
|
||||
},
|
||||
|
||||
/* 设置使用上下文 */
|
||||
setUsingContext(context: boolean) {
|
||||
this.usingContext = context
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 设置使用联网 */
|
||||
setUsingNetwork(context: boolean) {
|
||||
this.usingNetwork = context
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 删除当前对话组的全部内容 */
|
||||
async clearChatByGroupId() {
|
||||
if (!this.active)
|
||||
return
|
||||
|
||||
await fetchDelChatLogByGroupIdAPI({ groupId: this.active })
|
||||
await this.queryActiveChatLogList()
|
||||
},
|
||||
|
||||
recordState() {
|
||||
setLocalState(this.$state)
|
||||
},
|
||||
|
||||
clearChat() {
|
||||
this.chatList = []
|
||||
this.groupList = []
|
||||
this.active = 0
|
||||
this.recordState()
|
||||
},
|
||||
},
|
||||
})
|
||||
55
chat/src/store/modules/global/helper.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'userStorage'
|
||||
|
||||
export interface UserInfo {
|
||||
avatar: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface OrderInfo {
|
||||
pkgInfo: {
|
||||
id: number
|
||||
des: string
|
||||
name: string
|
||||
price: string
|
||||
model3Count: number
|
||||
model4Count: number
|
||||
drawMjCount: number
|
||||
coverImg: string
|
||||
days: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface GlobalState {
|
||||
payDialog: boolean
|
||||
goodsDialog: boolean
|
||||
fingerprint: number
|
||||
noticeDialog: boolean
|
||||
bindWxDialog: boolean
|
||||
signInDialog: boolean
|
||||
modelDialog: boolean
|
||||
isChatIn: boolean
|
||||
orderInfo: OrderInfo
|
||||
model: number
|
||||
iframeUrl: string
|
||||
clipboardText: string
|
||||
}
|
||||
|
||||
export function defaultSetting(): UserState {
|
||||
return {
|
||||
userInfo: {
|
||||
avatar: 'https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1681310872890image.png',
|
||||
name: '未登录',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalState(): UserState {
|
||||
const localSetting: UserState | undefined = ss.get(LOCAL_NAME)
|
||||
return { ...defaultSetting(), ...localSetting }
|
||||
}
|
||||
|
||||
export function setLocalState(setting: UserState): void {
|
||||
ss.set(LOCAL_NAME, setting)
|
||||
}
|
||||