初始化

This commit is contained in:
xiaoyi
2024-01-27 19:53:17 +08:00
commit 07dbe71c31
840 changed files with 119152 additions and 0 deletions

91
chat/src/App.vue Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
export interface ResData {
success: boolean
message: string
data: any
}

104
chat/src/api/user.ts Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
chat/src/assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
chat/src/assets/badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
chat/src/assets/fail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
chat/src/assets/img-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
chat/src/assets/market.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
chat/src/assets/qianbao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1 @@
[]

BIN
chat/src/assets/wechat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

BIN
chat/src/assets/wxpay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View 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>

View File

@@ -0,0 +1,3 @@
import TitleBar from './titleBar.vue'
export { TitleBar }

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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]">&nbsp;</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 }

View 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: '支付超时',
}

View 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 }
}

View 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,
}
}

View 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 }
}

View 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

File diff suppressed because one or more lines are too long

1
chat/src/icons/404.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.7 KiB

5
chat/src/icons/500.vue Normal file

File diff suppressed because one or more lines are too long

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View File

@@ -0,0 +1,3 @@
import Layout from './index.vue'
export { Layout }

125
chat/src/layout/index.vue Normal file
View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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()

View 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

View File

@@ -0,0 +1,4 @@
import setupAssets from './assets'
import setupScrollbarStyle from './scrollbarStyle'
export { setupAssets, setupScrollbarStyle }

View 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
View 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()
}

View 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
View 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'

View 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)
}

View 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)
}

View 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[]
}

View 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)
}

View 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
}

View 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)
}

View 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
}
})
}
})
}

View 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()
},
},
})

View 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)
}

Some files were not shown because too many files have changed in this diff Show More