v4.3.0
200
chat/src/App.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import favicon from '@/assets/favicon.ico'
|
||||
|
||||
import Watermark from '@/components/common/Watermark/index.vue'
|
||||
import HtmlDialog from '@/components/HtmlDialog.vue'
|
||||
import { initWechatLogin } from '@/services/wechatLogin' // 导入微信登录相关功能
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { DIALOG_TABS } from '@/store/modules/global'
|
||||
import { ClientJS } from 'clientjs'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ss } from './utils/storage'
|
||||
|
||||
const client = new ClientJS()
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const sharedHtml = ref('')
|
||||
|
||||
// 获取客户端指纹
|
||||
useGlobalStore.updateFingerprint(client.getFingerprint())
|
||||
|
||||
// 获取配置
|
||||
const faviconPath = computed(() => authStore.globalConfig?.clientFaviconPath || favicon)
|
||||
const isAutoOpenNotice = computed(() => Number(authStore.globalConfig?.isAutoOpenNotice) === 1)
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
const wechatSilentLoginStatus = computed(
|
||||
() => Number(authStore.globalConfig?.wechatSilentLoginStatus) === 1
|
||||
)
|
||||
const showWatermark = computed(() => Number(authStore.globalConfig?.showWatermark) === 1)
|
||||
// 默认不清除缓存,需要在后台配置中设置为1才开启自动清除
|
||||
const clearCacheEnabled = computed(() => Number(authStore.globalConfig?.clearCacheEnabled) === 1)
|
||||
|
||||
/**
|
||||
* 清除所有本地缓存
|
||||
* 包括localStorage、sessionStorage和indexedDB缓存
|
||||
* @param {boolean} forceClear 强制清除缓存,不考虑配置
|
||||
*/
|
||||
function clearAllCache(forceClear = false) {
|
||||
// 如果未启用清除缓存且未指定强制清除,则跳过
|
||||
if (!clearCacheEnabled.value && !forceClear) {
|
||||
console.log('缓存清除未启用')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前登录状态
|
||||
const isUserLoggedIn = authStore.isLogin
|
||||
// 如果用户已登录,则不清除缓存
|
||||
if (isUserLoggedIn) {
|
||||
console.log('用户已登录,跳过缓存清除')
|
||||
return
|
||||
}
|
||||
|
||||
// 以下是用户未登录时的缓存清除逻辑
|
||||
// 保存所有本地存储的关键数据
|
||||
const savedData: Record<string, string | null> = {}
|
||||
|
||||
// 保存主题
|
||||
savedData['theme'] = localStorage.getItem('theme') || 'light'
|
||||
|
||||
// 保存其他非登录相关但需要保留的数据
|
||||
const preserveKeys = ['appLanguage', 'agreedToUserAgreement']
|
||||
|
||||
// 保存这些数据
|
||||
preserveKeys.forEach(key => {
|
||||
const value = localStorage.getItem(key)
|
||||
if (value) savedData[key] = value
|
||||
|
||||
const ssValue = sessionStorage.getItem(key)
|
||||
if (ssValue) savedData[`ss_${key}`] = ssValue
|
||||
})
|
||||
|
||||
// 清除localStorage
|
||||
localStorage.clear()
|
||||
|
||||
// 清除sessionStorage
|
||||
sessionStorage.clear()
|
||||
|
||||
// 恢复所有保存的数据
|
||||
Object.keys(savedData).forEach(key => {
|
||||
const value = savedData[key]
|
||||
if (value !== null) {
|
||||
if (key.startsWith('ss_')) {
|
||||
// 恢复到sessionStorage
|
||||
sessionStorage.setItem(key.substring(3), value)
|
||||
} else {
|
||||
// 恢复到localStorage
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 清除indexedDB数据库
|
||||
if (window.indexedDB.databases) {
|
||||
window.indexedDB
|
||||
.databases()
|
||||
.then(databases => {
|
||||
databases.forEach(database => {
|
||||
if (database.name) {
|
||||
window.indexedDB.deleteDatabase(database.name)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
// 如果浏览器不支持databases()方法,使用兼容性方式处理
|
||||
console.warn('无法直接清除IndexedDB数据库')
|
||||
})
|
||||
}
|
||||
|
||||
// 清除应用缓存(如果支持)
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(keys => {
|
||||
keys.forEach(key => {
|
||||
caches.delete(key)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
console.log('本地缓存已清除')
|
||||
}
|
||||
|
||||
async function loadBaiduCode() {
|
||||
const baiduCode = authStore.globalConfig?.baiduCode
|
||||
if (!baiduCode) return
|
||||
const scriptElem = document.createElement('script')
|
||||
scriptElem.innerHTML = baiduCode.replace(/<script[\s\S]*?>([\s\S]*?)<\/script>/gi, '$1')
|
||||
document.head.appendChild(scriptElem)
|
||||
}
|
||||
|
||||
function setDocumentTitle() {
|
||||
document.title = authStore.globalConfig?.siteName || 'AI'
|
||||
}
|
||||
|
||||
function noticeInit() {
|
||||
const showNotice = ss.get('showNotice')
|
||||
if ((!showNotice || Date.now() > Number(showNotice)) && isAutoOpenNotice.value) {
|
||||
useGlobalStore.updateSettingsDialog(true, DIALOG_TABS.NOTICE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前浏览器是否是微信内置浏览器
|
||||
* 区分微信和企业微信,只有微信浏览器返回true
|
||||
* @returns {boolean} 如果是微信浏览器返回 true,否则返回 false
|
||||
*/
|
||||
function isWechatBrowser(): boolean {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
|
||||
// 检查是否是企业微信
|
||||
const isWXWork = ua.indexOf('wxwork') !== -1
|
||||
|
||||
// 检查是否是微信,排除企业微信的情况
|
||||
const isWeixin = !isWXWork && ua.indexOf('micromessenger') !== -1
|
||||
|
||||
return isWeixin
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 设置网站标题
|
||||
setDocumentTitle()
|
||||
|
||||
// 加载百度统计代码(如果有)
|
||||
loadBaiduCode()
|
||||
|
||||
// 如果开启微信静默登录,并且是微信浏览器,执行微信登录逻辑
|
||||
if (wechatSilentLoginStatus.value && isWechatBrowser()) {
|
||||
await initWechatLogin() // 初始化微信登录
|
||||
}
|
||||
|
||||
// 尝试自动清除缓存
|
||||
clearAllCache()
|
||||
|
||||
/* 动态设置网站ico svg格式 */
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'shortcut icon'
|
||||
link.href = faviconPath.value
|
||||
link.type = 'image/png' // 设置正确的图像类型
|
||||
|
||||
// 移除已存在的favicon链接,防止冲突
|
||||
const existingFavicons = document.querySelectorAll('link[rel="shortcut icon"], link[rel="icon"]')
|
||||
existingFavicons.forEach(node => node.parentNode?.removeChild(node))
|
||||
|
||||
// 添加新的favicon链接
|
||||
document.head.appendChild(link)
|
||||
// 初始化通知提示
|
||||
await noticeInit()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 水印组件(如果启用) -->
|
||||
<Watermark v-if="showWatermark"></Watermark>
|
||||
|
||||
<!-- 主要内容使用router-view -->
|
||||
<router-view />
|
||||
|
||||
<!-- 共享内容对话框 -->
|
||||
<HtmlDialog :visible="useGlobalStore.htmlDialog" :html="sharedHtml" />
|
||||
|
||||
<!-- 全局图片预览器 -->
|
||||
<GlobalImageViewer />
|
||||
</template>
|
||||
40
chat/src/api/appStore.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSearchAppsAPI<T>(data: { keyword: string }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/app/searchList',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 查询个人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 })
|
||||
}
|
||||
|
||||
/* 查询单个分类 */
|
||||
export function fetchQueryOneCatAPI<T>(data): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/app/queryOneCat',
|
||||
data,
|
||||
})
|
||||
}
|
||||
28
chat/src/api/balance.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchVisitorCountAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/balance/getVisitorCount',
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSyncVisitorDataAPI<T>(): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/balance/inheritVisitorData',
|
||||
})
|
||||
}
|
||||
54
chat/src/api/chatLog.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* 删除对话记录 */
|
||||
export function fetchDelChatLogAPI<T>(data: { id: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/chatlog/del',
|
||||
// url: '/chatlog/deleteChatsAfterId',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 删除一组对话记录 */
|
||||
export function fetchDelChatLogByGroupIdAPI<T>(data: { groupId: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/chatlog/delByGroupId',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 删除一组对话记录 */
|
||||
export function fetchDeleteGroupChatsAfterIdAPI<T>(data: { id: number }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/chatlog/deleteChatsAfterId',
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/* 查询单条消息的状态和内容 */
|
||||
export function fetchQuerySingleChatLogAPI<T>(data: { chatId: number }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/chatlog/querySingleChat',
|
||||
data,
|
||||
})
|
||||
}
|
||||
9
chat/src/api/config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/* query globe config */
|
||||
export function fetchQueryConfigAPI<T>(data: any) {
|
||||
return get<T>({
|
||||
url: '/config/queryFront',
|
||||
data,
|
||||
})
|
||||
}
|
||||
21
chat/src/api/crami.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* use crami */
|
||||
export function fetchUseCramiAPI<T>(data: { code: string }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/crami/useCrami',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get all crami package */
|
||||
export function fetchGetPackageAPI<T>(data: {
|
||||
status: number
|
||||
type?: number
|
||||
size?: number
|
||||
}): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/crami/queryAllPackage',
|
||||
data,
|
||||
})
|
||||
}
|
||||
8
chat/src/api/global.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/* get notice */
|
||||
export function fetchGetGlobalNoticeAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/config/notice',
|
||||
})
|
||||
}
|
||||
53
chat/src/api/group.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* 创建新的对话组 */
|
||||
export function fetchCreateGroupAPI<T>(data?: {
|
||||
appId?: number
|
||||
modelConfig?: any
|
||||
params?: string
|
||||
}): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/group/create',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 查询对话组列表 */
|
||||
export function fetchQueryGroupAPI<T>(): Promise<T> {
|
||||
return get<T>({ url: '/group/query' })
|
||||
}
|
||||
|
||||
/* 通过groupId查询当前对话组的详细信息 */
|
||||
export function fetchGroupInfoById<T>(groupId: number | string): Promise<T> {
|
||||
return get<T>({ url: `/group/info/${groupId}` })
|
||||
}
|
||||
|
||||
/* 修改对话组 */
|
||||
export function fetchUpdateGroupAPI<T>(data?: {
|
||||
groupId?: number
|
||||
title?: string
|
||||
isSticky?: boolean
|
||||
config?: string
|
||||
fileUrl?: 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,
|
||||
})
|
||||
}
|
||||
181
chat/src/api/index.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
import { fetchStream } from '@/utils/request/fetch'
|
||||
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
|
||||
|
||||
/* 对话聊天 */
|
||||
export function fetchChatAPIProcess<T = any>(params: {
|
||||
model: string
|
||||
modelName: string
|
||||
modelType: number
|
||||
modelAvatar?: string
|
||||
prompt: string
|
||||
sslUrl?: string
|
||||
chatId?: string
|
||||
fileInfo?: string
|
||||
imageUrl?: string
|
||||
fileUrl?: string
|
||||
action?: string
|
||||
drawId?: string
|
||||
customId?: string
|
||||
appId?: number
|
||||
extraParam?: { size?: string }
|
||||
usingPluginId?: number
|
||||
options?: {
|
||||
groupId: number
|
||||
usingNetwork: boolean
|
||||
usingMcpTool: boolean
|
||||
}
|
||||
signal?: GenericAbortSignal
|
||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
taskId?: string
|
||||
}) {
|
||||
// 准备请求数据
|
||||
const data = {
|
||||
model: params.model,
|
||||
modelName: params.modelName,
|
||||
modelType: params.modelType,
|
||||
prompt: params.prompt,
|
||||
fileInfo: params?.fileInfo,
|
||||
imageUrl: params?.imageUrl,
|
||||
fileUrl: params?.fileUrl,
|
||||
extraParam: params?.extraParam,
|
||||
appId: params?.appId,
|
||||
options: params.options,
|
||||
action: params?.action,
|
||||
customId: params?.customId,
|
||||
usingPluginId: params?.usingPluginId,
|
||||
drawId: params?.drawId,
|
||||
modelAvatar: params?.modelAvatar,
|
||||
taskId: params?.taskId,
|
||||
}
|
||||
|
||||
// 如果没有进度回调,则使用普通POST请求
|
||||
if (!params.onDownloadProgress) {
|
||||
return post<T>({
|
||||
url: '/chatgpt/chat-process',
|
||||
data,
|
||||
signal: params.signal,
|
||||
})
|
||||
}
|
||||
|
||||
// 使用流式请求处理
|
||||
return new Promise((resolve, reject) => {
|
||||
// 创建AbortController用于取消请求
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
|
||||
// 如果提供了signal,添加到请求选项
|
||||
if (params.signal) {
|
||||
// 由于类型不兼容,这里使用类型断言
|
||||
fetchOptions.signal = params.signal as any
|
||||
}
|
||||
|
||||
fetchStream('/chatgpt/chat-process', fetchOptions, chunk => {
|
||||
// 调用进度回调,模拟axios的onDownloadProgress事件
|
||||
if (params.onDownloadProgress) {
|
||||
// 创建一个符合AxiosProgressEvent的对象
|
||||
const progressEvent: AxiosProgressEvent = {
|
||||
event: {
|
||||
target: {
|
||||
responseText: chunk,
|
||||
getResponseHeader: (name: string) => null,
|
||||
},
|
||||
} as any,
|
||||
loaded: chunk.length,
|
||||
total: 0, // 流式响应无法预知总长度
|
||||
bytes: chunk.length,
|
||||
lengthComputable: false,
|
||||
progress: 0,
|
||||
}
|
||||
params.onDownloadProgress(progressEvent)
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
resolve({ data: response } as any)
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPptCoverAPIProcess<T>(data: {
|
||||
color?: string
|
||||
style?: string
|
||||
title: string
|
||||
}): Promise<T> {
|
||||
return post<T>({ url: '/chatgpt/ppt-cover', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* TTS 文字转语音 */
|
||||
export function fetchTtsAPIProcess<T>(data: { chatId: number; prompt: string }): Promise<T> {
|
||||
return post<T>({ url: '/chatgpt/tts-process', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 获取个人信息 */
|
||||
export function fetchGetInfo<T>() {
|
||||
return get<T>({ url: '/auth/getInfo' })
|
||||
}
|
||||
|
||||
/* 注册 */
|
||||
export function fetchRegisterAPI<T>(data: {
|
||||
username: string
|
||||
password: string
|
||||
contact: string
|
||||
code: string
|
||||
}): Promise<T> {
|
||||
return post<T>({ url: '/auth/register', 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 fetchLoginWithCaptchaAPI<T>(data: { contact: string; code: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/loginWithCaptcha', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 修改个人信息 */
|
||||
export function fetchUpdateInfoAPI<T>(data: {
|
||||
username?: string
|
||||
avatar?: string
|
||||
nickname?: string
|
||||
}): Promise<T> {
|
||||
return post<T>({ url: '/user/update', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 修改密码 */
|
||||
export function fetchUpdatePasswordAPI<T>(data: { password?: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/updatePassword', 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 fetchSendCode<T>(data: { contact: string; captchaCode: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/sendCode', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 发送手机验证码 */
|
||||
export function fetchSendSms<T>(data: { phone: string }): Promise<T> {
|
||||
return post<T>({ url: '/auth/sendPhoneCode', data }) as Promise<T>
|
||||
}
|
||||
|
||||
/* 发送邮箱验证码 */
|
||||
export function fetchSendEmailCode<T>(data: {
|
||||
phone: string
|
||||
captchaId: string
|
||||
captchaCode: string
|
||||
}): Promise<T> {
|
||||
return post<T>({ url: '/auth/sendEmailCode', data }) as Promise<T>
|
||||
}
|
||||
15
chat/src/api/models.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/* query models list */
|
||||
export function fetchQueryModelsListAPI<T>() {
|
||||
return get<T>({
|
||||
url: '/models/list',
|
||||
})
|
||||
}
|
||||
|
||||
/* query base model config */
|
||||
export function fetchModelBaseConfigAPI<T>() {
|
||||
return get<T>({
|
||||
url: '/models/baseConfig',
|
||||
})
|
||||
}
|
||||
17
chat/src/api/order.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* order buy */
|
||||
export function fetchOrderBuyAPI<T>(data: { goodsId: number; payType?: string }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/order/buy',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* order query */
|
||||
export function fetchOrderQueryAPI<T>(data: { orderId: string }): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/order/queryByOrderId',
|
||||
data,
|
||||
})
|
||||
}
|
||||
8
chat/src/api/plugin.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/* 查询全量app列表 */
|
||||
export function fetchQueryPluginsAPI<T>(): Promise<any> {
|
||||
return get<T>({
|
||||
url: '/plugin/pluginList',
|
||||
})
|
||||
}
|
||||
15
chat/src/api/share.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* order buy */
|
||||
export function createShare<T>(data: { htmlContent: string }): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/share/create',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function getShare<T>(shareCode: string): Promise<T> {
|
||||
return get<T>({
|
||||
url: `/share/${shareCode}`,
|
||||
})
|
||||
}
|
||||
15
chat/src/api/signin.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* sign in */
|
||||
export function fetchSignInAPI<T>(): Promise<T> {
|
||||
return post<T>({
|
||||
url: '/signin/sign',
|
||||
})
|
||||
}
|
||||
|
||||
/* sign log */
|
||||
export function fetchSignLogAPI<T>(): Promise<T> {
|
||||
return get<T>({
|
||||
url: '/signin/signinLog',
|
||||
})
|
||||
}
|
||||
5
chat/src/api/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ResData {
|
||||
success: boolean
|
||||
message: string
|
||||
data: any
|
||||
}
|
||||
30
chat/src/api/upload.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Response } from '@/utils/request'
|
||||
import { post } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 上传单个文件
|
||||
* @param file 要上传的文件
|
||||
* @param dir 上传目录,默认使用当前日期目录
|
||||
*/
|
||||
export function uploadFile<T = any>(file: File, dir?: string): Promise<Response<T>> {
|
||||
// 如果未提供目录,使用默认的日期格式目录
|
||||
if (!dir) {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const currentDate = `${year}${month}/${day}`
|
||||
dir = `userFiles/${currentDate}`
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
|
||||
const path = `/upload/file?dir=${encodeURIComponent(dir)}`
|
||||
|
||||
return post<T>({
|
||||
url: path,
|
||||
data: form,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
}
|
||||
116
chat/src/api/user.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { Response } from '@/utils/request'
|
||||
import { get, post } from '@/utils/request'
|
||||
|
||||
/* get wechat-login senceStr */
|
||||
export function fetchGetQRSceneStrAPI<T>(data: {}): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/getQRSceneStr',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wechat-login qr url */
|
||||
export function fetchGetQRCodeAPI<T>(data: { sceneStr: string }): Promise<Response<T>> {
|
||||
return get<T>({
|
||||
url: '/official/getQRCode',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* login by scenceStr */
|
||||
export function fetchLoginBySceneStrAPI<T>(data: { sceneStr: string }): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/loginBySceneStr',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* login by code */
|
||||
export function fetchLoginByCodeAPI<T>(data: { code: string }): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/loginByCode',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wx registery config */
|
||||
export function fetchGetJsapiTicketAPI<T>(data: { url: string }): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/getJsapiTicket',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wechat-login senceStr */
|
||||
export function fetchGetQRSceneStrByBindAPI<T>(): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/getQRSceneStrByBind',
|
||||
})
|
||||
}
|
||||
|
||||
/* bind wx by scenceStr */
|
||||
export function fetchBindWxBySceneStrAPI<T>(data: { sceneStr: string }): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/bindWxBySceneStr',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* get wx rediriect login url */
|
||||
export function fetchWxLoginRedirectAPI<T>(data: { url: string }): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/getRedirectUrl',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 实名认证 */
|
||||
export function fetchVerifyIdentityAPI<T>(data: {
|
||||
name: string
|
||||
idCard: string
|
||||
}): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/auth/verifyIdentity',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 手机认证 */
|
||||
export function fetchVerifyPhoneIdentityAPI<T>(data: {
|
||||
phone: string
|
||||
username: string
|
||||
password: string
|
||||
code: string
|
||||
}): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/auth/verifyPhoneIdentity',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 获取旧账号迁移二维码的sceneStr */
|
||||
export function fetchGetQRSceneStrByOldWechatAPI<T>(): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/getQRSceneStrByOldWechat',
|
||||
})
|
||||
}
|
||||
|
||||
/* 轮询查询旧微信迁移结果 */
|
||||
export function fetchBindWxByOldWechatAPI<T>(data: { sceneStr: string }): Promise<Response<T>> {
|
||||
return post<T>({
|
||||
url: '/official/bindWxByOldWechat',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/* 获取旧公众号二维码(用于账号迁移)
|
||||
* 注意:此接口与普通二维码接口区别在于它返回的是旧公众号的二维码
|
||||
* 后端API: /official/getOldQRCode
|
||||
*/
|
||||
export function fetchGetOldQRCodeAPI<T>(data: { sceneStr: string }): Promise<Response<T>> {
|
||||
console.log('[API调试] 调用getOldQRCode接口, 参数:', data)
|
||||
return get<T>({
|
||||
url: '/official/getOldQRCode',
|
||||
data, // 使用data字段传递参数,会被转换为URL参数
|
||||
})
|
||||
}
|
||||
BIN
chat/src/assets/aiavatar/360logo.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
chat/src/assets/aiavatar/alilogo.png
Executable file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
chat/src/assets/aiavatar/baidulogo.png
Executable file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
chat/src/assets/aiavatar/claudelogo.png
Executable file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
chat/src/assets/aiavatar/dalle.png
Executable file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
chat/src/assets/aiavatar/google.gif
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
chat/src/assets/aiavatar/gpt4logo.png
Executable file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
chat/src/assets/aiavatar/midjourney.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
chat/src/assets/aiavatar/mindmap.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
chat/src/assets/aiavatar/network.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
1
chat/src/assets/aiavatar/openai.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg" style="flex: 0 0 auto; line-height: 1;"><title>OpenAI</title><path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
chat/src/assets/aiavatar/sdxl.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
chat/src/assets/aiavatar/suno.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
chat/src/assets/aiavatar/tencentlogo.png
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
chat/src/assets/aiavatar/xunfeilogo.png
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
chat/src/assets/aiavatar/zhipulogo.png
Executable file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
chat/src/assets/alipay.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
chat/src/assets/avatar.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
chat/src/assets/badge.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
710
chat/src/assets/defaultPreset.json
Normal file
@@ -0,0 +1,710 @@
|
||||
[
|
||||
{
|
||||
"title": "英语翻译官",
|
||||
"prompt": "我希望你能担任英语翻译、拼写校对和修辞改进的角色。我会用任何语言和你交流,你会识别语言,将其翻译并用更为优美和精炼的英语回答我。请将我简单的词汇和句子替换成更为优美和高雅的表达方式,确保意思不变,但使其更具文学性。请仅回答更正和改进的部分,不要写解释。我的第一句话是“how are you ?”,请翻译它。",
|
||||
"icon": "ri:ai-generate"
|
||||
},
|
||||
{
|
||||
"title": "心理学家",
|
||||
"prompt": "我想让你扮演一个心理学家。我会告诉你我的想法。我希望你能给我科学的建议,让我感觉更好。我的第一个想法,{ 在这里输入你的想法,如果你解释得更详细,我想你会得到更准确的答案。}",
|
||||
"icon": "ri:heart-line"
|
||||
},
|
||||
{
|
||||
"title": "产品经理",
|
||||
"prompt": "请确认我的以下请求。请您作为产品经理回复我。我将会提供一个主题,您将帮助我编写一份包括以下章节标题的PRD文档:主题、简介、问题陈述、目标与目的、用户故事、技术要求、收益、KPI指标、开发风险以及结论。在我要求具体主题、功能或开发的PRD之前,请不要先写任何一份PRD文档。",
|
||||
"icon": "ri:projector-line"
|
||||
},
|
||||
{
|
||||
"title": "如何学做菜",
|
||||
"prompt": "我要你做我的私人厨师。我会告诉你我的饮食偏好和过敏,你会建议我尝试的食谱。你应该只回复你推荐的食谱,别无其他。不要写解释。我的第一个请求是“我是一名素食主义者,我正在寻找健康的晚餐点子。”",
|
||||
"icon": "ri:restaurant-line"
|
||||
},
|
||||
{
|
||||
"title": "规划一个去上海的旅游攻略 参观博物馆",
|
||||
"prompt": "我想让你做一个旅游指南。我会把我的位置写给你,你会推荐一个靠近我的位置的地方。在某些情况下,我还会告诉您我将访问的地方类型。您还会向我推荐靠近我的第一个位置的类似类型的地方。我的第一个建议请求是“我在上海,我只想参观博物馆。”",
|
||||
"icon": "ri:map-pin-line"
|
||||
},
|
||||
{
|
||||
"title": "穿越时空",
|
||||
"prompt": "如果你能穿越时空,你会去哪个时代?",
|
||||
"icon": "ri:time-line"
|
||||
},
|
||||
{
|
||||
"title": "量子力学",
|
||||
"prompt": "解释一下量子力学是什么?",
|
||||
"icon": "ri:flask-line"
|
||||
},
|
||||
{
|
||||
"title": "人工智能",
|
||||
"prompt": "介绍一下人工智能的历史",
|
||||
"icon": "ri:robot-line"
|
||||
},
|
||||
{
|
||||
"title": "深度学习",
|
||||
"prompt": "讲解一下深度学习是如何工作的?",
|
||||
"icon": "ri:brain-line"
|
||||
},
|
||||
{
|
||||
"title": "冯诺依曼体系结构",
|
||||
"prompt": "请举例说明什么是冯诺依曼体系结构?",
|
||||
"icon": "ri:computer-line"
|
||||
},
|
||||
{
|
||||
"title": "红楼梦情感分析",
|
||||
"prompt": "请分析《红楼梦》中林黛玉与贾宝玉的情感关系。",
|
||||
"icon": "ri:book-2-line"
|
||||
},
|
||||
{
|
||||
"title": "100米短跑训练",
|
||||
"prompt": "如何训练才能提高100米短跑成绩?",
|
||||
"icon": "ri:run-line"
|
||||
},
|
||||
{
|
||||
"title": "北京旅游攻略",
|
||||
"prompt": "请推荐一份适合初次来中国的外国人的北京旅游攻略。",
|
||||
"icon": "ri:road-map-line"
|
||||
},
|
||||
{
|
||||
"title": "低GI饮食",
|
||||
"prompt": "什么是低GI饮食?这种饮食有哪些好处?",
|
||||
"icon": "ri:restaurant-2-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "全球环境问题",
|
||||
"prompt": "请列出目前全球主要面临的三大环境问题,并简单阐述其影响和应对措施。",
|
||||
"icon": "ri:earth-line"
|
||||
},
|
||||
{
|
||||
"title": "提高社交影响力",
|
||||
"prompt": "在社交场合,如何提高自己的感染力和影响力?",
|
||||
"icon": "ri:team-line"
|
||||
},
|
||||
{
|
||||
"title": "地中海地理特征",
|
||||
"prompt": "请描述一下地中海的地理特征,以及这些特征对于古代世界的影响。",
|
||||
"icon": "ri:map-pin-line"
|
||||
},
|
||||
{
|
||||
"title": "《肖申克的救赎》影评",
|
||||
"prompt": "请评价电影《肖申克的救赎》的剧情、角色塑造和拍摄手法。",
|
||||
"icon": "ri:film-line"
|
||||
},
|
||||
{
|
||||
"title": "苹果公司成功分析",
|
||||
"prompt": "为什么苹果公司的产品总是比其他公司的产品更受欢迎?请从市场策略、产品设计、品牌形象等方面进行分析。",
|
||||
"icon": "ri:apple-line"
|
||||
},
|
||||
{
|
||||
"title": "健康饮食计划",
|
||||
"prompt": "如何制定一份健康的饮食计划?",
|
||||
"icon": "ri:heart-line"
|
||||
},
|
||||
{
|
||||
"title": "编程学习指南",
|
||||
"prompt": "怎样学习编程?",
|
||||
"icon": "ri:code-line"
|
||||
},
|
||||
{
|
||||
"title": "巴厘岛旅游景点",
|
||||
"prompt": "在巴厘岛旅游有哪些值得参观的景点?",
|
||||
"icon": "ri:map-pin-2-line"
|
||||
},
|
||||
{
|
||||
"title": "处理亲密关系分歧",
|
||||
"prompt": "如何处理亲密关系中的分歧?",
|
||||
"icon": "ri:heart-2-line"
|
||||
},
|
||||
{
|
||||
"title": "费马大定理证明",
|
||||
"prompt": "如何证明费马大定理?",
|
||||
"icon": "ri:function-line"
|
||||
},
|
||||
{
|
||||
"title": "吸烟相关疾病预防",
|
||||
"prompt": "长期吸烟引起的疾病有哪些?应该如何预防?",
|
||||
"icon": "ri:lungs-line"
|
||||
},
|
||||
{
|
||||
"title": "克服拖延症",
|
||||
"prompt": "如何克服拖延症?",
|
||||
"icon": "ri:time-line"
|
||||
},
|
||||
{
|
||||
"title": "减少家庭垃圾",
|
||||
"prompt": "如何减少家庭垃圾产生?",
|
||||
"icon": "ri:recycle-line"
|
||||
},
|
||||
{
|
||||
"title": "股票价值评估",
|
||||
"prompt": "如何评估股票的价值?",
|
||||
"icon": "ri:stock-line"
|
||||
},
|
||||
{
|
||||
"title": "自信的社交表现",
|
||||
"prompt": "如何在社交场合自信地表现自己?",
|
||||
"icon": "ri:team-line"
|
||||
},
|
||||
{
|
||||
"title": "推荐科幻电影",
|
||||
"prompt": "给我一个最近评分不错的科幻电影的名字和简介",
|
||||
"icon": "ri:movie-line"
|
||||
},
|
||||
{
|
||||
"title": "英文翻译校对",
|
||||
"prompt": "将下面这句英文翻译成中文并纠正其中的语法错误:'Me and him goes to the store yesterday.'",
|
||||
"icon": "ri:translate-2",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "科技类大市值股票",
|
||||
"prompt": "给我一些市值超过1000亿美元的科技类股票",
|
||||
"icon": "ri:bar-chart-box-line"
|
||||
},
|
||||
{
|
||||
"title": "商品销售量预测",
|
||||
"prompt": "基于历史销售数据,预测下周某商品的销售量。",
|
||||
"icon": "ri:line-chart-line",
|
||||
"iconColor": "text-cyan-500"
|
||||
},
|
||||
{
|
||||
"title": "思念诗歌创作",
|
||||
"prompt": "请用七言绝句写一首表达思念之情的诗歌。",
|
||||
"icon": "ri:quill-pen-line"
|
||||
},
|
||||
{
|
||||
"title": "情侣约会餐厅推荐",
|
||||
"prompt": "给我一个适合情侣约会的餐厅的名字和地址。",
|
||||
"icon": "ri:restaurant-2-line"
|
||||
},
|
||||
{
|
||||
"title": "西班牙旅游行程规划",
|
||||
"prompt": "我计划去西班牙旅游,请帮我安排一个10天的行程。",
|
||||
"icon": "ri:suitcase-3-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "电影分类归类",
|
||||
"prompt": "将电影从爱情片、动作片和恐怖片三种分类中分别归类。",
|
||||
"icon": "ri:film-line"
|
||||
},
|
||||
{
|
||||
"title": "豆腐美食推荐",
|
||||
"prompt": "推荐一道以豆腐为主要原料的美食,附上制作方法。",
|
||||
"icon": "ri:restaurant-line"
|
||||
},
|
||||
{
|
||||
"title": "流行华语歌曲推荐",
|
||||
"prompt": "推荐最近流行的三首华语歌曲,并简要介绍它们的风格和歌词主题。",
|
||||
"icon": "ri:music-line"
|
||||
},
|
||||
{
|
||||
"title": "减少塑料污染生活指南",
|
||||
"prompt": "请提供三条减少塑料污染的生活指南。",
|
||||
"icon": "ri:leaf-line"
|
||||
},
|
||||
{
|
||||
"title": "团队合作处理矛盾",
|
||||
"prompt": "如何在团队合作中处理与同事之间的矛盾?",
|
||||
"icon": "ri:team-line"
|
||||
},
|
||||
{
|
||||
"title": "前景股票投资",
|
||||
"prompt": "你认为现在买入哪些股票比较有前景?",
|
||||
"icon": "ri:stock-line"
|
||||
},
|
||||
{
|
||||
"title": "科幻片推荐",
|
||||
"prompt": "你能否给我推荐一部最近上映的好看的科幻片?",
|
||||
"icon": "ri:film-line"
|
||||
},
|
||||
{
|
||||
"title": "三亚旅游攻略",
|
||||
"prompt": "希望去三亚旅游,你能提供一份详细的旅游攻略吗?",
|
||||
"icon": "ri:suitcase-2-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "意大利面烹饪技巧",
|
||||
"prompt": "我想学做意大利面,你有什么简单易学的做法推荐吗?",
|
||||
"icon": "ri:restaurant-line"
|
||||
},
|
||||
{
|
||||
"title": "缓解焦虑的方法",
|
||||
"prompt": "我感到很紧张,有什么方法能够缓解焦虑吗?",
|
||||
"icon": "ri:heart-pulse-line"
|
||||
},
|
||||
{
|
||||
"title": "电商平台投诉处理",
|
||||
"prompt": "我在某电商平台购买的商品质量不佳,该如何向平台进行投诉处理?",
|
||||
"icon": "ri:feedback-line"
|
||||
},
|
||||
{
|
||||
"title": "有效学外语的方法",
|
||||
"prompt": "你觉得学外语最有效的方法是什么?",
|
||||
"icon": "ri:translate-2"
|
||||
},
|
||||
{
|
||||
"title": "职场发展建议",
|
||||
"prompt": "我正在寻找新的工作机会,有哪些职业领域前景较好?",
|
||||
"icon": "ri:briefcase-line",
|
||||
"iconColor": "text-cyan-500"
|
||||
},
|
||||
{
|
||||
"title": "日本旅游攻略",
|
||||
"prompt": "提供至少三个去日本旅游必去的景点,并描述其特色和适合的旅游时间。",
|
||||
"icon": "ri:map-pin-line"
|
||||
},
|
||||
{
|
||||
"title": "提高保险销售业绩",
|
||||
"prompt": "如何提高保险销售员的业绩?",
|
||||
"icon": "ri:money-dollar-box-line"
|
||||
},
|
||||
{
|
||||
"title": "公司网站改版建议",
|
||||
"prompt": "公司网站需要进行改版,请列举至少五个需要更新的页面元素并说明更新的理由。",
|
||||
"icon": "ri:layout-5-line"
|
||||
},
|
||||
{
|
||||
"title": "印度首都查询",
|
||||
"prompt": "请问印度的首都是哪里?",
|
||||
"icon": "ri:flag-line"
|
||||
},
|
||||
{
|
||||
"title": "红旗渠修建历史",
|
||||
"prompt": "请问红旗渠修建的时间和地点分别是什么?",
|
||||
"icon": "ri:history-line"
|
||||
},
|
||||
{
|
||||
"title": "DNA结构与功能",
|
||||
"prompt": "请简要介绍一下DNA的结构及其功能。",
|
||||
"icon": "ri:dna-line"
|
||||
},
|
||||
{
|
||||
"title": "GDP定义与计算",
|
||||
"prompt": "请问什么是GDP?如何计算GDP?",
|
||||
"icon": "ri:bar-chart-2-line"
|
||||
},
|
||||
{
|
||||
"title": "原子核组成",
|
||||
"prompt": "请问原子核由哪些粒子组成?它们各自的电荷和质量分别是多少?",
|
||||
"icon": "ri:leaf-line"
|
||||
},
|
||||
{
|
||||
"title": "莫扎特代表作",
|
||||
"prompt": "请问莫扎特的代表作有哪些?",
|
||||
"icon": "ri:music-2-line"
|
||||
},
|
||||
{
|
||||
"title": "汉字词源",
|
||||
"prompt": "请问“汉字”这个词最早出现的时间和在哪本书中出现的?",
|
||||
"icon": "ri:book-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "全运会历史",
|
||||
"prompt": "请问全运会是哪年开始举办的?每隔几年举办一次?",
|
||||
"icon": "ri:football-line"
|
||||
},
|
||||
{
|
||||
"title": "石油用途",
|
||||
"prompt": "请问石油的主要用途有哪些?",
|
||||
"icon": "ri:oil-line"
|
||||
},
|
||||
{
|
||||
"title": "心脏起搏器介绍",
|
||||
"prompt": "请简要介绍一下心脏起搏器的原理和使用方法。",
|
||||
"icon": "ri:heart-2-line",
|
||||
"iconColor": "text-cyan-500"
|
||||
},
|
||||
{
|
||||
"title": "观众情感分析",
|
||||
"prompt": "这部电影的观众反应如何?",
|
||||
"icon": "ri:emotion-laugh-line"
|
||||
},
|
||||
{
|
||||
"title": "沙滩美景短文",
|
||||
"prompt": "请写出一篇描述橙色阳光下沙滩美景的短文。",
|
||||
"icon": "ri:sun-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "亚马逊财报数据查询",
|
||||
"prompt": "亚马逊公司的年度财报数据是多少?",
|
||||
"icon": "ri:money-dollar-box-line"
|
||||
},
|
||||
{
|
||||
"title": "苹果新产品新闻",
|
||||
"prompt": "请问最近有关于苹果公司新发布产品的新闻吗?",
|
||||
"icon": "ri:apple-line"
|
||||
},
|
||||
{
|
||||
"title": "一加与华为手机性能对比",
|
||||
"prompt": "请比较一加手机和华为手机的性能差异。",
|
||||
"icon": "ri:smartphone-line"
|
||||
},
|
||||
{
|
||||
"title": "文章主要观点提取",
|
||||
"prompt": "请从这篇文章中提取出主要观点。",
|
||||
"icon": "ri:article-line"
|
||||
},
|
||||
{
|
||||
"title": "用户意图分类",
|
||||
"prompt": "用户输入“我想要预定机票”,它的意图是什么?",
|
||||
"icon": "ri:question-line"
|
||||
},
|
||||
{
|
||||
"title": "文章可读性修改",
|
||||
"prompt": "请编辑这篇文章,使得它更易读。",
|
||||
"icon": "ri:edit-line"
|
||||
},
|
||||
{
|
||||
"title": "星期推理",
|
||||
"prompt": "如果今天是星期三,那么后天是星期几?",
|
||||
"icon": "ri:calendar-line",
|
||||
"iconColor": "text-cyan-500"
|
||||
},
|
||||
{
|
||||
"title": "微软创始人查询",
|
||||
"prompt": "谁创办了微软公司?",
|
||||
"icon": "ri:building-4-line"
|
||||
},
|
||||
{
|
||||
"title": "电影类型分类",
|
||||
"prompt": "这个电影是哪个类型的?",
|
||||
"icon": "ri:film-line"
|
||||
},
|
||||
{
|
||||
"title": "乐器描述",
|
||||
"prompt": "描述一下你最喜欢的乐器。",
|
||||
"icon": "ri:music-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "句子改写",
|
||||
"prompt": "请改写这句话:“天空飘着几朵云彩。”",
|
||||
"icon": "ri:edit-2-line"
|
||||
},
|
||||
{
|
||||
"title": "书籍对比",
|
||||
"prompt": "这本书和那本书有什么区别?",
|
||||
"icon": "ri:book-line"
|
||||
},
|
||||
{
|
||||
"title": "自然风景描写",
|
||||
"prompt": "写一段自然风景的描写。",
|
||||
"icon": "ri:landscape-line"
|
||||
},
|
||||
{
|
||||
"title": "音乐年代分类",
|
||||
"prompt": "这首歌曲属于哪个年代的音乐?",
|
||||
"icon": "ri:music-2-line"
|
||||
},
|
||||
{
|
||||
"title": "餐厅美食对比",
|
||||
"prompt": "这家餐厅和那家餐厅哪家更好吃?",
|
||||
"icon": "ri:restaurant-line"
|
||||
},
|
||||
{
|
||||
"title": "电影喜好",
|
||||
"prompt": "把这句话翻译成英文:“我喜欢看电影,尤其是科幻电影。”",
|
||||
"icon": "ri:movie-line"
|
||||
},
|
||||
{
|
||||
"title": "理想度假胜地描述",
|
||||
"prompt": "描述一下你理想中的度假胜地。",
|
||||
"icon": "ri:tree-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "动物分类",
|
||||
"prompt": "这个动物属于哪个门类?",
|
||||
"icon": "ri:bug-line"
|
||||
},
|
||||
{
|
||||
"title": "新闻摘要生成",
|
||||
"prompt": "请问如何利用 GPT-3.5 生成一篇 100 字左右的新闻摘要?",
|
||||
"icon": "ri:newspaper-line"
|
||||
},
|
||||
{
|
||||
"title": "自动翻译实现",
|
||||
"prompt": "请问如何让 GPT-3.5 实现从中文到英文的自动翻译?",
|
||||
"icon": "ri:translate"
|
||||
},
|
||||
{
|
||||
"title": "全球医疗保健评价",
|
||||
"prompt": "你如何评价当前全球范围内的医疗保健体系?",
|
||||
"icon": "ri:stethoscope-line"
|
||||
},
|
||||
{
|
||||
"title": "文化多样性保护",
|
||||
"prompt": "请问有哪些国家在法律层面上保护本国的文化多样性?",
|
||||
"icon": "ri:global-line"
|
||||
},
|
||||
{
|
||||
"title": "新能源普及国家",
|
||||
"prompt": "现今世界上使用新能源最为普及的国家是哪些?",
|
||||
"icon": "ri:flashlight-line"
|
||||
},
|
||||
{
|
||||
"title": "股市走势预测",
|
||||
"prompt": "你认为全球股市未来一个季度会走势如何?",
|
||||
"icon": "ri:line-chart-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "前沿科技研究",
|
||||
"prompt": "请列举一些目前全球前沿的科技研究领域。",
|
||||
"icon": "ri:rocket-line"
|
||||
},
|
||||
{
|
||||
"title": "社交媒体影响",
|
||||
"prompt": "社交媒体对年轻人的影响有哪些?",
|
||||
"icon": "ri:chat-3-line"
|
||||
},
|
||||
{
|
||||
"title": "电商平台市场份额",
|
||||
"prompt": "当前哪些电商平台在全球拥有最大的市场份额?",
|
||||
"icon": "ri:shopping-cart-line",
|
||||
"iconColor": "text-cyan-500"
|
||||
},
|
||||
{
|
||||
"title": "气候变化影响",
|
||||
"prompt": "气候变化对世界各地造成了哪些影响?",
|
||||
"icon": "ri:sun-cloudy-line"
|
||||
},
|
||||
{
|
||||
"title": "全球顶尖大学排名",
|
||||
"prompt": "请问哪些国家拥有全球最顶尖的大学排名?",
|
||||
"icon": "ri:school-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "手机发明者",
|
||||
"prompt": "手机是谁发明的?",
|
||||
"icon": "ri:smartphone-line"
|
||||
},
|
||||
{
|
||||
"title": "旅行故事创作",
|
||||
"prompt": "给我写一个关于旅行的故事。",
|
||||
"icon": "ri:suitcase-3-line"
|
||||
},
|
||||
{
|
||||
"title": "文章情感分析",
|
||||
"prompt": "这篇文章中的情感倾向是积极、消极还是中性?",
|
||||
"icon": "ri:emotion-line"
|
||||
},
|
||||
{
|
||||
"title": "拼写错误纠正",
|
||||
"prompt": "句子中的哪个单词拼写有误:“昨天我去了餐馆,品尝了他们的招牌菜。”",
|
||||
"icon": "ri:check-line"
|
||||
},
|
||||
{
|
||||
"title": "文章摘要生成",
|
||||
"prompt": "请为这篇长文章生成一段简要的摘要。",
|
||||
"icon": "ri:file-text-line"
|
||||
},
|
||||
{
|
||||
"title": "任务执行指令",
|
||||
"prompt": "请告诉我现在怎么做。",
|
||||
"icon": "ri:task-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "明朝社会阶层研究",
|
||||
"prompt": "针对明朝时期的社会阶层结构,你能列出几种不同的人群并描述他们的特征吗?",
|
||||
"icon": "ri:book-line",
|
||||
"iconColor": "text-brown-500"
|
||||
},
|
||||
{
|
||||
"title": "物种区别解释",
|
||||
"prompt": "两个相似物种的区别在哪里?请用一种易于理解的方式解释。",
|
||||
"icon": "ri:leaf-line"
|
||||
},
|
||||
{
|
||||
"title": "政治参与度分析",
|
||||
"prompt": "哪些因素影响政治参与度?你认为如何激发公民参与政治?",
|
||||
"icon": "ri:government-line"
|
||||
},
|
||||
{
|
||||
"title": "情感分析技术",
|
||||
"prompt": "如何利用自然语言处理技术进行情感分析?您可以列举一些常见的情感分析算法和应用场景吗?",
|
||||
"icon": "ri:emotion-line"
|
||||
},
|
||||
{
|
||||
"title": "经济发展水平衡量",
|
||||
"prompt": "如何衡量一个国家的经济发展水平?您如何评估不同国家之间的贸易关系?",
|
||||
"icon": "ri:money-dollar-circle-line"
|
||||
},
|
||||
{
|
||||
"title": "机器学习简介",
|
||||
"prompt": "讲述一下什么是机器学习,以及它在现代计算机科学中扮演的角色。",
|
||||
"icon": "ri:robot-line"
|
||||
},
|
||||
{
|
||||
"title": "气候变化影响",
|
||||
"prompt": "近年来,气候变化对我们的环境造成了哪些影响?未来还可能会引起哪些灾难?",
|
||||
"icon": "ri:sun-cloudy-line"
|
||||
},
|
||||
{
|
||||
"title": "创新教育方法",
|
||||
"prompt": "教师应该如何培养学生的创新思维和实践能力?您认为有效的教育方法是什么?",
|
||||
"icon": "ri:lightbulb-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "学习心理素质",
|
||||
"prompt": "学习一门新技能需要哪些心理素质?如何在学习过程中保持积极的情绪状态?",
|
||||
"icon": "ri:psychotherapy-line"
|
||||
},
|
||||
{
|
||||
"title": "未来科技趋势",
|
||||
"prompt": "未来科技发展的趋势是什么?您认为会有哪些领域会得到革命性的改变?",
|
||||
"icon": "ri:rocket-line"
|
||||
},
|
||||
{
|
||||
"title": "电影推荐",
|
||||
"prompt": "根据我的口味推荐一部近期上映的电影。",
|
||||
"icon": "ri:film-line"
|
||||
},
|
||||
{
|
||||
"title": "手机产品比较",
|
||||
"prompt": "请分析一下 iPhone 和 Android 手机的优缺点,说明它们适合不同的用户群体。",
|
||||
"icon": "ri:smartphone-line"
|
||||
},
|
||||
{
|
||||
"title": "新闻头条创作",
|
||||
"prompt": "请为明天的头条新闻写一个简短但有吸引力的标题,并提出三个相关问题。",
|
||||
"icon": "ri:newspaper-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "市场零食品牌分析",
|
||||
"prompt": "请列举五种最受欢迎的零食品牌,并分析其在市场上的竞争优势。",
|
||||
"icon": "ri:shopping-bag-line"
|
||||
},
|
||||
{
|
||||
"title": "自然之美短文",
|
||||
"prompt": "请根据以下关键词写一篇题为“自然之美”的 300 字左右的短文:山水、湖泊、森林、鸟儿、日出日落。",
|
||||
"icon": "ri:palette-line"
|
||||
},
|
||||
{
|
||||
"title": "英文文本编辑",
|
||||
"prompt": "翻译以下这段英文,同时对其进行适当的调整和编辑:He was lay down.",
|
||||
"icon": "ri:edit-line"
|
||||
},
|
||||
{
|
||||
"title": "近期电影推荐",
|
||||
"prompt": "可以给我推荐几部最近比较值得观看的电影吗?",
|
||||
"icon": "ri:film-line"
|
||||
},
|
||||
{
|
||||
"title": "马克思主义知识问答",
|
||||
"prompt": "马克思主义的基本原理是什么?",
|
||||
"icon": "ri:questionnaire-line"
|
||||
},
|
||||
{
|
||||
"title": "北京旅游攻略",
|
||||
"prompt": "如果想去北京旅游,有哪些必去的景点和美食呢?",
|
||||
"icon": "ri:road-map-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "经济形势分析",
|
||||
"prompt": "分析一下目前国内外经济形势,对未来的发展有何预测?",
|
||||
"icon": "ri:line-chart-line"
|
||||
},
|
||||
{
|
||||
"title": "文章情感分类",
|
||||
"prompt": "这篇文章是正面的还是负面的?",
|
||||
"icon": "ri:emotion-line"
|
||||
},
|
||||
{
|
||||
"title": "写作效率提升方法",
|
||||
"prompt": "有哪些方法可以提高写作效率?",
|
||||
"icon": "ri:keyboard-box-line"
|
||||
},
|
||||
{
|
||||
"title": "电子书与纸质书对比",
|
||||
"prompt": "阅读电子书和纸质书有什么区别?",
|
||||
"icon": "ri:book-2-line"
|
||||
},
|
||||
{
|
||||
"title": "论文语法修改",
|
||||
"prompt": "请帮我修改这篇论文中的语法错误。",
|
||||
"icon": "ri:file-edit-line"
|
||||
},
|
||||
{
|
||||
"title": "人工智能知识查询",
|
||||
"prompt": "什么是人工智能?",
|
||||
"icon": "ri:robot-line"
|
||||
},
|
||||
{
|
||||
"title": "实体识别",
|
||||
"prompt": "在这段文字中,'苹果'指的是手机品牌还是水果?",
|
||||
"icon": "ri:barcode-box-line"
|
||||
},
|
||||
{
|
||||
"title": "文章主题分类",
|
||||
"prompt": "这篇文章的主题是什么?",
|
||||
"icon": "ri:layout-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "文章摘要生成",
|
||||
"prompt": "请用一句话概括这篇文章的核心内容。",
|
||||
"icon": "ri:file-text-line"
|
||||
},
|
||||
{
|
||||
"title": "新上映电影推荐",
|
||||
"prompt": "有哪些值得一看的新上映电影?",
|
||||
"icon": "ri:movie-line"
|
||||
},
|
||||
{
|
||||
"title": "欧洲杯赛程",
|
||||
"prompt": "请列出近期欧洲杯足球赛程表。",
|
||||
"icon": "ri:trophy-line",
|
||||
"iconColor": "text-gold-500"
|
||||
},
|
||||
{
|
||||
"title": "健康饮食方案",
|
||||
"prompt": "有哪些适合控制体重的健康饮食方案?",
|
||||
"icon": "ri:restaurant-line",
|
||||
"iconColor": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"title": "日本旅游攻略",
|
||||
"prompt": "如果我想去日本旅游,应该怎样规划我的行程和预算?",
|
||||
"icon": "ri:suitcase-line"
|
||||
},
|
||||
{
|
||||
"title": "最新科技新闻",
|
||||
"prompt": "有哪些最近的科技进展值得关注?",
|
||||
"icon": "ri:news-line"
|
||||
},
|
||||
{
|
||||
"title": "编程语言选择",
|
||||
"prompt": "当你需要开发一个新项目时,该如何选择合适的编程语言?",
|
||||
"icon": "ri:code-box-line"
|
||||
},
|
||||
{
|
||||
"title": "健康饮食搭配",
|
||||
"prompt": "请问在平衡健康饮食方面,应该怎样搭配膳食结构?",
|
||||
"icon": "ri:restaurant-2-line"
|
||||
},
|
||||
{
|
||||
"title": "科技公司伦理标准",
|
||||
"prompt": "微软、谷歌等科技公司是否有明确的伦理标准?如果有,请简要列举这些标准。",
|
||||
"icon": "ri:shield-check-line"
|
||||
},
|
||||
{
|
||||
"title": "机器人研究方向",
|
||||
"prompt": "机器人研究领域都包括哪些方向?",
|
||||
"icon": "ri:robot-line"
|
||||
},
|
||||
{
|
||||
"title": "气候变化影响",
|
||||
"prompt": "你认为气候变化对人类有哪些不利影响?",
|
||||
"icon": "ri:cloud-windy-line"
|
||||
}
|
||||
]
|
||||
BIN
chat/src/assets/fail.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
chat/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
chat/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
chat/src/assets/market.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
chat/src/assets/reset.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
chat/src/assets/wechat.png
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
chat/src/assets/wxpay.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
101
chat/src/components/BadWordsDialog.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import 'md-editor-v3/lib/preview.css'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
|
||||
const countdown = ref(15) // 倒计时 15 秒
|
||||
const isCountdownFinished = ref(false) // 倒计时结束标志
|
||||
|
||||
function startCountdown() {
|
||||
const interval = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value -= 1
|
||||
} else {
|
||||
isCountdownFinished.value = true // 倒计时结束
|
||||
clearInterval(interval) // 清除定时器
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function handleAgree() {
|
||||
if (isCountdownFinished.value) {
|
||||
useGlobalStore.UpdateBadWordsDialog(false) // 关闭用户协议弹窗
|
||||
countdown.value = 15
|
||||
isCountdownFinished.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// onMounted(() => {
|
||||
// startCountdown(); // 组件挂载时启动倒计时
|
||||
// });
|
||||
|
||||
// 监听 props.visible 的变化,当其变为 true 时重新启动倒计时
|
||||
watch(
|
||||
() => props.visible,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
startCountdown()
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 添加 immediate 选项,初始化时立即执行
|
||||
)
|
||||
|
||||
// 初始启动一次倒计时
|
||||
if (props.visible) {
|
||||
startCountdown()
|
||||
}
|
||||
|
||||
// onUnmounted(() => {
|
||||
// // countdown.value = 15;
|
||||
// isCountdownFinished.value = false; // 重置状态
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="props.visible"
|
||||
class="fixed inset-0 z-50 px-2 flex items-center justify-center bg-black bg-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900 p-4 rounded-lg shadow-lg w-full max-w-3xl max-h-[80vh] flex flex-col relative"
|
||||
>
|
||||
<!-- 显示用户协议标题 -->
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-xl">合理合规须知</span>
|
||||
</div>
|
||||
|
||||
<!-- 直接显示用户协议的 HTML 内容 -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<!-- <div
|
||||
v-html="globalConfig.agreementInfo"
|
||||
class="dark:bg-gray-900 p-4"
|
||||
></div> -->
|
||||
<p>请合理合规使用,请勿咨询敏感信息或使用敏感词生成图片。</p>
|
||||
<p>
|
||||
多次触发平台风控,将记录【账号/IP】等信息并禁止使用,保留向有关部门提交相关记录的权利。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 倒计时和同意按钮 -->
|
||||
<div class="flex justify-end mt-3">
|
||||
<button
|
||||
:disabled="!isCountdownFinished"
|
||||
@click="handleAgree"
|
||||
class="px-4 py-2 shadow-sm bg-primary-600 text-white rounded-md hover:bg-primary-500 disabled:bg-gray-400"
|
||||
>
|
||||
<span v-if="isCountdownFinished">已知晓</span>
|
||||
<span v-else>请等待 {{ countdown }} 秒</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
92
chat/src/components/CloseButtonDemo.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="p-6 space-y-4 dark:bg-gray-800">
|
||||
<h1 class="text-xl font-bold dark:text-white">关闭按钮演示</h1>
|
||||
|
||||
<div class="flex items-center space-x-6">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm dark:text-gray-300">小尺寸</p>
|
||||
<button class="btn-close btn-close-sm">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm dark:text-gray-300">中尺寸</p>
|
||||
<button class="btn-close btn-close-md">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm dark:text-gray-300">大尺寸</p>
|
||||
<button class="btn-close btn-close-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border rounded-lg dark:border-gray-700 relative">
|
||||
<h2 class="text-lg font-semibold dark:text-white">模态框示例</h2>
|
||||
<p class="mt-2 dark:text-gray-300">这是一个带有关闭按钮的模态框示例</p>
|
||||
<button class="btn-close btn-close-md absolute top-3 right-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 无需特别的逻辑
|
||||
</script>
|
||||
99
chat/src/components/Dialog/Confirm.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<Teleport to="body" :disabled="!visible">
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="transform opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="transform opacity-0"
|
||||
>
|
||||
<div v-if="visible" class="fixed inset-0 z-[9999]">
|
||||
<!-- 遮罩层 -->
|
||||
<div class="absolute inset-0 bg-black/50" @click="handleCancel"></div>
|
||||
|
||||
<!-- 对话框 -->
|
||||
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<div
|
||||
class="w-[400px] bg-white dark:bg-[#24272e] rounded-lg shadow-lg overflow-hidden"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 标题 -->
|
||||
<div class="p-4 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h3 class="text-lg font-medium text-neutral-900 dark:text-neutral-100">
|
||||
{{ options.title }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="p-4 text-neutral-700 dark:text-neutral-300">
|
||||
{{ options.content }}
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<div class="flex justify-end gap-2 px-4 py-3">
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded-md border border-neutral-200 dark:border-neutral-700 hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-700 dark:text-neutral-300 transition-colors"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ options.negativeText }}
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded-md bg-primary-500 hover:bg-primary-600 text-white transition-colors"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ options.positiveText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DialogOptions } from '@/utils/dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const visible = ref(false)
|
||||
const resolvePromise: any = ref(null)
|
||||
const options = ref<DialogOptions>({
|
||||
title: '',
|
||||
content: '',
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
})
|
||||
|
||||
const showDialog = (dialogOptions: DialogOptions) => {
|
||||
options.value = {
|
||||
...options.value,
|
||||
...dialogOptions,
|
||||
}
|
||||
visible.value = true
|
||||
return new Promise(resolve => {
|
||||
resolvePromise.value = resolve
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
if (options.value.onPositiveClick) {
|
||||
await options.value.onPositiveClick()
|
||||
}
|
||||
visible.value = false
|
||||
resolvePromise.value(true)
|
||||
} catch (e) {
|
||||
resolvePromise.value(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
resolvePromise.value(false)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
})
|
||||
</script>
|
||||
101
chat/src/components/Dialogs/BadWordsDialog.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import 'md-editor-v3/lib/preview.css'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
|
||||
const countdown = ref(15) // 倒计时 15 秒
|
||||
const isCountdownFinished = ref(false) // 倒计时结束标志
|
||||
|
||||
function startCountdown() {
|
||||
const interval = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value -= 1
|
||||
} else {
|
||||
isCountdownFinished.value = true // 倒计时结束
|
||||
clearInterval(interval) // 清除定时器
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function handleAgree() {
|
||||
if (isCountdownFinished.value) {
|
||||
useGlobalStore.UpdateBadWordsDialog(false) // 关闭用户协议弹窗
|
||||
countdown.value = 15
|
||||
isCountdownFinished.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// onMounted(() => {
|
||||
// startCountdown(); // 组件挂载时启动倒计时
|
||||
// });
|
||||
|
||||
// 监听 props.visible 的变化,当其变为 true 时重新启动倒计时
|
||||
watch(
|
||||
() => props.visible,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
startCountdown()
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 添加 immediate 选项,初始化时立即执行
|
||||
)
|
||||
|
||||
// 初始启动一次倒计时
|
||||
if (props.visible) {
|
||||
startCountdown()
|
||||
}
|
||||
|
||||
// onUnmounted(() => {
|
||||
// // countdown.value = 15;
|
||||
// isCountdownFinished.value = false; // 重置状态
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="props.visible"
|
||||
class="fixed inset-0 z-[10001] px-2 flex items-center justify-center bg-black bg-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900 p-4 rounded-lg shadow-lg w-full max-w-3xl max-h-[80vh] flex flex-col relative"
|
||||
>
|
||||
<!-- 显示用户协议标题 -->
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-xl">合理合规须知</span>
|
||||
</div>
|
||||
|
||||
<!-- 直接显示用户协议的 HTML 内容 -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<!-- <div
|
||||
v-html="globalConfig.agreementInfo"
|
||||
class="dark:bg-gray-900 p-4"
|
||||
></div> -->
|
||||
<p>请合理合规使用,请勿咨询敏感信息或使用敏感词生成图片。</p>
|
||||
<p>
|
||||
多次触发平台风控,将记录【账号/IP】等信息并禁止使用,保留向有关部门提交相关记录的权利。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 倒计时和同意按钮 -->
|
||||
<div class="flex justify-end mt-3">
|
||||
<button
|
||||
:disabled="!isCountdownFinished"
|
||||
@click="handleAgree"
|
||||
class="px-4 py-2 shadow-sm bg-primary-600 text-white rounded-md hover:bg-primary-500 disabled:bg-gray-400"
|
||||
>
|
||||
<span v-if="isCountdownFinished">已知晓</span>
|
||||
<span v-else>请等待 {{ countdown }} 秒</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
327
chat/src/components/HtmlDialog.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<script setup lang="ts">
|
||||
import { createShare } from '@/api/share'
|
||||
import type { ResData } from '@/api/types'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useGlobalStoreWithOut } from '@/store'
|
||||
import { message } from '@/utils/message'
|
||||
import { html } from '@codemirror/lang-html'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { oneDark } from '@codemirror/theme-one-dark'
|
||||
import { Close } from '@icon-park/vue-next'
|
||||
import { EditorView, basicSetup } from 'codemirror'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
html?: string
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['update:visible', 'update:html'])
|
||||
const globalStore = useGlobalStoreWithOut()
|
||||
const ms = message()
|
||||
const htmlPreviewRef = ref<HTMLIFrameElement | null>(null)
|
||||
const localEditableText = ref(props.html || '')
|
||||
const { isMobile } = useBasicLayout()
|
||||
const editorContainerRef = ref<HTMLDivElement | null>(null)
|
||||
const isDarkMode = computed(() => document.documentElement.classList.contains('dark'))
|
||||
|
||||
// CodeMirror 编辑器实例
|
||||
let editor: EditorView | null = null
|
||||
|
||||
// 当props.html变化时更新本地编辑文本和编辑器
|
||||
watchEffect(() => {
|
||||
if (props.visible) {
|
||||
// Update local ref first
|
||||
if (props.html && props.html !== localEditableText.value) {
|
||||
localEditableText.value = props.html
|
||||
}
|
||||
|
||||
// Update editor content if editor exists and content differs
|
||||
if (editor) {
|
||||
const currentContent = editor.state.doc.toString()
|
||||
if (localEditableText.value !== currentContent) {
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: localEditableText.value || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 使用 watchEffect 来更新预览,当 localEditableText 变化时
|
||||
watchEffect(() => {
|
||||
if (props.visible) {
|
||||
// 只在可见时更新预览
|
||||
updatePreview()
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化编辑器
|
||||
const initializeEditor = () => {
|
||||
if (!editorContainerRef.value || editor) return // 防止重复初始化
|
||||
|
||||
const extensions = [
|
||||
basicSetup,
|
||||
html(),
|
||||
EditorView.updateListener.of(update => {
|
||||
if (update.docChanged) {
|
||||
const newContent = update.state.doc.toString()
|
||||
localEditableText.value = newContent
|
||||
emit('update:html', newContent) // Optional: emit update immediately
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
if (isDarkMode.value) {
|
||||
extensions.push(oneDark)
|
||||
}
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: localEditableText.value || '',
|
||||
extensions,
|
||||
})
|
||||
|
||||
editor = new EditorView({
|
||||
state,
|
||||
parent: editorContainerRef.value,
|
||||
})
|
||||
}
|
||||
|
||||
// 预览更新逻辑 (保持不变或根据需要调整)
|
||||
const updatePreview = () => {
|
||||
if (htmlPreviewRef.value) {
|
||||
// 更新 iframe 的 srcDoc 更为推荐,避免潜在的 XSS
|
||||
htmlPreviewRef.value.srcdoc = localEditableText.value
|
||||
/* 或者保持原来的方式,如果需要执行脚本等
|
||||
const iframeDocument = htmlPreviewRef.value.contentDocument
|
||||
if (iframeDocument) {
|
||||
iframeDocument.open()
|
||||
iframeDocument.write(localEditableText.value)
|
||||
iframeDocument.close()
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
// 阻止事件冒泡和默认行为
|
||||
emit('update:visible', false)
|
||||
emit('update:html', localEditableText.value)
|
||||
globalStore.updateHtmlDialog(false)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
const success = await copyToClipboard(localEditableText.value)
|
||||
if (success) {
|
||||
ms.success('内容已复制到剪贴板')
|
||||
} else {
|
||||
// 复制失败时,提示用户手动复制
|
||||
ms.info('复制失败,请手动复制文本框中的内容')
|
||||
}
|
||||
} catch (err) {
|
||||
ms.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleShare = async () => {
|
||||
try {
|
||||
const res: ResData = await createShare({
|
||||
htmlContent: localEditableText.value,
|
||||
})
|
||||
|
||||
const shareCode = res.data.shareCode
|
||||
const success = await copyToClipboard(shareCode)
|
||||
if (success) {
|
||||
ms.success('分享链接已复制到剪贴板')
|
||||
} else {
|
||||
localEditableText.value = shareCode
|
||||
ms.info('复制失败,分享链接已显示在文本框中,请手动复制')
|
||||
}
|
||||
} catch (err) {
|
||||
ms.error('分享失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容性更好的剪贴板复制方法
|
||||
const copyToClipboard = async (text: string): Promise<boolean> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载和卸载逻辑
|
||||
onMounted(() => {
|
||||
// 如果初始可见,则初始化编辑器
|
||||
if (props.visible) {
|
||||
nextTick(initializeEditor)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (editor) {
|
||||
editor.destroy()
|
||||
editor = null
|
||||
}
|
||||
})
|
||||
|
||||
// 监听可见性变化来创建/销毁编辑器
|
||||
watch(
|
||||
() => props.visible,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
// 弹窗变为可见,延迟初始化编辑器以确保DOM可用
|
||||
nextTick(() => {
|
||||
if (!editor && editorContainerRef.value) {
|
||||
initializeEditor()
|
||||
}
|
||||
// 确保预览是最新的
|
||||
updatePreview()
|
||||
})
|
||||
} else {
|
||||
// 弹窗关闭时,无需立即销毁编辑器,onUnmounted 会处理
|
||||
// 但可以考虑保存状态等操作
|
||||
emit('update:html', localEditableText.value) // 确保关闭时内容已更新
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="props.visible"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center html-modal-container"
|
||||
>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50" @click.stop="handleClose"></div>
|
||||
|
||||
<Close
|
||||
class="absolute top-3 right-3 cursor-pointer z-30"
|
||||
size="18"
|
||||
@click.stop.prevent="handleClose"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="relative bg-white dark:bg-gray-900 w-full h-full p-4 z-10"
|
||||
:class="[isMobile ? 'flex-col' : 'flex']"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 移动端预览区域 -->
|
||||
<div v-if="isMobile" class="p-2 w-full h-1/2">
|
||||
<iframe
|
||||
ref="htmlPreviewRef"
|
||||
:srcDoc="localEditableText"
|
||||
class="box-border w-full h-full border rounded-md"
|
||||
frameborder="0"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<div
|
||||
v-if="props.editable !== false"
|
||||
class="p-2 flex flex-col"
|
||||
:class="[isMobile ? 'w-full h-1/2' : 'w-1/4']"
|
||||
>
|
||||
<!-- CodeMirror 编辑器容器 -->
|
||||
<div
|
||||
ref="editorContainerRef"
|
||||
class="w-full h-full border rounded-md overflow-hidden dark:border-gray-700 code-editor-container"
|
||||
></div>
|
||||
|
||||
<div class="mt-2 flex justify-end">
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="px-4 py-2 shadow-sm ring-1 ring-inset bg-white ring-gray-300 hover:bg-gray-50 text-gray-900 rounded-md mr-4 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:ring-gray-700 dark:hover:ring-gray-600"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="handleCopy"
|
||||
class="px-4 py-2 shadow-sm bg-primary-600 hover:bg-primary-500 text-white dark rounded-md mr-4"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
<button
|
||||
@click="handleShare"
|
||||
class="px-4 py-2 shadow-sm bg-primary-600 hover:bg-primary-500 text-white dark rounded-md"
|
||||
>
|
||||
分享
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端预览区域 -->
|
||||
<div v-if="!isMobile" :class="[props.editable === false ? 'w-full' : 'w-3/4']" class="p-2">
|
||||
<iframe
|
||||
ref="htmlPreviewRef"
|
||||
:srcDoc="localEditableText"
|
||||
class="box-border w-full h-full border rounded-md"
|
||||
frameborder="0"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fixed {
|
||||
position: fixed;
|
||||
-webkit-position: fixed;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* CodeMirror编辑器容器样式 (从 PythonDialog.vue 复制并调整) */
|
||||
.code-editor-container {
|
||||
height: calc(100% - 40px); /* Adjust height based on button container */
|
||||
}
|
||||
|
||||
.code-editor-container :deep(.cm-editor) {
|
||||
height: 100% !important;
|
||||
font-family: 'Fira Code', 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.code-editor-container :deep(.cm-scroller) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-editor-container :deep(.cm-gutters) {
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.code-editor-container :deep(.dark .cm-gutters) {
|
||||
border-right: 1px solid #444;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.code-editor-container :deep(.cm-activeLineGutter) {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.code-editor-container :deep(.dark .cm-activeLineGutter) {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.code-editor-container :deep(.cm-focused) {
|
||||
outline: none !important;
|
||||
}
|
||||
</style>
|
||||
165
chat/src/components/Identity.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
|
||||
import { fetchVerifyIdentityAPI } from '@/api/user'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { Close } from '@icon-park/vue-next'
|
||||
|
||||
import { message } from '@/utils/message'
|
||||
import { computed, ref } from 'vue'
|
||||
import Vcode from './Login/SliderCaptcha.vue'
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const ms = message()
|
||||
const { isMobile } = useBasicLayout()
|
||||
const isShow = ref(false)
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const authStore = useAuthStore()
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
|
||||
const identityForm = ref({
|
||||
name: '',
|
||||
idCard: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
idCard: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
// 使用 ref 来管理全局参数的状态
|
||||
const agreedToUserAgreement = ref(true) // 读取初始状态并转换为布尔类型
|
||||
|
||||
// 点击"用户协议及隐私政策"时,自动同意
|
||||
function handleClick() {
|
||||
agreedToUserAgreement.value = true // 设置为同意
|
||||
}
|
||||
|
||||
function handlerSubmit() {
|
||||
if (agreedToUserAgreement.value === false && globalConfig.value.isAutoOpenAgreement === '1') {
|
||||
return ms.error(`请阅读并同意《${globalConfig.value.agreementTitle}》`)
|
||||
}
|
||||
isShow.value = false
|
||||
fetchVerifyIdentityAPI(identityForm.value).then(res => {
|
||||
if (res.code === 200) {
|
||||
ms.success('认证成功')
|
||||
useGlobalStore.updateIdentityDialog(false)
|
||||
} else {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-[10001] flex flex-col items-center justify-center bg-black bg-opacity-50 py-6"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded-lg shadow-lg w-full max-h-[70vh] flex flex-col dark:bg-gray-900 dark:text-gray-400 relative"
|
||||
:class="{ 'max-w-[95vw]': isMobile, 'max-w-xl': !isMobile }"
|
||||
>
|
||||
<Close
|
||||
size="18"
|
||||
class="absolute top-3 right-3 cursor-pointer z-30"
|
||||
@click="useGlobalStore.updateIdentityDialog(false)"
|
||||
/>
|
||||
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div
|
||||
class="flex w-full flex-col h-full justify-center"
|
||||
:class="isMobile ? 'px-5 py-5' : 'px-10 py-5'"
|
||||
>
|
||||
<!-- forget passwd-->
|
||||
<form
|
||||
ref="formRef"
|
||||
:model="identityForm"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<h2
|
||||
class="mb-8 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
实名认证
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label
|
||||
for="username"
|
||||
class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300"
|
||||
>姓名
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
v-model="identityForm.name"
|
||||
placeholder="请输入姓名"
|
||||
class="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label
|
||||
for="username"
|
||||
class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300"
|
||||
>身份证号
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
v-model="identityForm.idCard"
|
||||
placeholder="请输入身份证号"
|
||||
class="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="globalConfig.isAutoOpenAgreement === '1'"
|
||||
class="flex items-center justify-between my-3"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
v-model="agreedToUserAgreement"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
/>
|
||||
<p class="ml-1 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
已阅读并同意
|
||||
<a
|
||||
href="#"
|
||||
class="font-semibold leading-6 text-primary-600 hover:text-primary-500 dark:text-primary-500 dark:hover:text-primary-600"
|
||||
@click="handleClick"
|
||||
>《{{ globalConfig.agreementTitle }}》</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
@click="isShow = true"
|
||||
type="submit"
|
||||
class="flex w-full my-5 justify-center rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
提交认证
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Vcode :show="isShow" @success="handlerSubmit()" @close="isShow = false" class="bg-red-500" />
|
||||
</div>
|
||||
</template>
|
||||
390
chat/src/components/Login/Email.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<script lang="ts" setup>
|
||||
import { fetchLoginAPI, fetchSendCode } from '@/api'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { DIALOG_TABS } from '@/store/modules/global'
|
||||
import { message } from '@/utils/message'
|
||||
import { computed, ref } from 'vue'
|
||||
import SliderCaptcha from './SliderCaptcha.vue'
|
||||
|
||||
interface Props {
|
||||
loginMode: 'password' | 'captcha'
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const formRef = ref<HTMLFormElement | null>(null)
|
||||
const ms = message()
|
||||
const loading = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const lastSendPhoneCodeTime = ref(0)
|
||||
const { isMobile } = useBasicLayout()
|
||||
const isShow = ref(false)
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
|
||||
// 验证码登录表单
|
||||
const captchaForm = ref({
|
||||
contact: '',
|
||||
captchaId: null,
|
||||
code: '',
|
||||
})
|
||||
|
||||
// 密码登录表单
|
||||
const passwordForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
// 验证表单
|
||||
const validateForm = () => {
|
||||
let hasError = false
|
||||
|
||||
// 密码登录表单验证
|
||||
if (props.loginMode === 'password') {
|
||||
// 验证用户名
|
||||
if (!passwordForm.value.username.trim()) {
|
||||
hasError = true
|
||||
} else if (passwordForm.value.username.length < 2 || passwordForm.value.username.length > 30) {
|
||||
hasError = true
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!passwordForm.value.password.trim()) {
|
||||
hasError = true
|
||||
} else if (passwordForm.value.password.length < 6 || passwordForm.value.password.length > 30) {
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
// 验证码登录表单验证
|
||||
else if (props.loginMode === 'captcha') {
|
||||
// 验证联系方式
|
||||
if (!captchaForm.value.contact.trim()) {
|
||||
hasError = true
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if (!captchaForm.value.captchaId) {
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
return !hasError
|
||||
}
|
||||
|
||||
// 只验证联系方式,用于发送验证码前的验证
|
||||
const validateContactOnly = () => {
|
||||
return captchaForm.value.contact.trim() !== ''
|
||||
}
|
||||
|
||||
const phoneLoginStatus = computed(() => Number(authStore.globalConfig.phoneLoginStatus) === 1)
|
||||
const emailLoginStatus = computed(() => Number(authStore.globalConfig.emailLoginStatus) === 1)
|
||||
|
||||
// 使用 ref 来管理全局参数的状态
|
||||
const agreedToUserAgreement = ref(true) // 读取初始状态并转换为布尔类型
|
||||
|
||||
// 点击"用户协议及隐私政策"时,自动同意
|
||||
function handleClick() {
|
||||
agreedToUserAgreement.value = true // 设置为同意
|
||||
useGlobalStore.updateSettingsDialog(true, DIALOG_TABS.AGREEMENT)
|
||||
}
|
||||
|
||||
const loginTypeText = computed(() => {
|
||||
if (emailLoginStatus.value && phoneLoginStatus.value) {
|
||||
return t('login.emailPhone')
|
||||
} else if (emailLoginStatus.value) {
|
||||
return t('login.email')
|
||||
} else if (phoneLoginStatus.value) {
|
||||
return t('login.phone')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const loginEnterType = computed(() => {
|
||||
if (emailLoginStatus.value && phoneLoginStatus.value) {
|
||||
return t('login.enterEmailOrPhone')
|
||||
} else if (emailLoginStatus.value) {
|
||||
return t('login.enterEmail')
|
||||
} else if (phoneLoginStatus.value) {
|
||||
return t('login.enterPhone')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// 定时器改变倒计时时间方法
|
||||
function changeLastSendPhoneCodeTime() {
|
||||
if (lastSendPhoneCodeTime.value > 0) {
|
||||
setTimeout(() => {
|
||||
lastSendPhoneCodeTime.value--
|
||||
changeLastSendPhoneCodeTime()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
/* 发送验证码 */
|
||||
async function handleSendCaptcha() {
|
||||
isShow.value = false
|
||||
if (validateContactOnly()) {
|
||||
// 只验证联系方式
|
||||
try {
|
||||
const { contact } = captchaForm.value
|
||||
|
||||
// 只传递联系方式(邮箱或手机号)
|
||||
const params: any = { contact }
|
||||
let res: any
|
||||
res = await fetchSendCode(params)
|
||||
const { success } = res
|
||||
if (success) {
|
||||
ms.success(res.data)
|
||||
// 记录重新发送倒计时
|
||||
lastSendPhoneCodeTime.value = 60
|
||||
changeLastSendPhoneCodeTime()
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录处理 */
|
||||
function handlerSubmit(event: Event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (agreedToUserAgreement.value === false && globalConfig.value.isAutoOpenAgreement === '1') {
|
||||
return ms.error(`请阅读并同意《${globalConfig.value.agreementTitle}》`)
|
||||
}
|
||||
|
||||
if (validateForm()) {
|
||||
loginAction()
|
||||
}
|
||||
}
|
||||
|
||||
async function loginAction() {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 根据登录模式构建参数
|
||||
const params: any =
|
||||
props.loginMode === 'password'
|
||||
? {
|
||||
username: passwordForm.value.username,
|
||||
password: passwordForm.value.password,
|
||||
}
|
||||
: {
|
||||
username: captchaForm.value.contact,
|
||||
captchaId: captchaForm.value.captchaId,
|
||||
}
|
||||
|
||||
const res: any = await fetchLoginAPI(params)
|
||||
loading.value = false
|
||||
|
||||
const { success } = res
|
||||
|
||||
if (!success) return
|
||||
|
||||
ms.success(t('login.loginSuccess'))
|
||||
authStore.setToken(res.data)
|
||||
authStore.getUserInfo()
|
||||
authStore.setLoginDialog(false)
|
||||
} catch (error: any) {
|
||||
loading.value = false
|
||||
ms.error(error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col justify-between" :class="isMobile ? 'px-5 ' : 'px-10 '">
|
||||
<!-- 密码登录表单 -->
|
||||
<form
|
||||
v-if="loginMode === 'password'"
|
||||
ref="formRef"
|
||||
class="flex flex-col flex-1 justify-between"
|
||||
@submit="handlerSubmit"
|
||||
>
|
||||
<div>
|
||||
<!-- 用户名输入框 -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
for="username"
|
||||
class="block text-sm/6 font-medium text-gray-900 dark:text-gray-300"
|
||||
>{{ loginTypeText }}</label
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
v-model="passwordForm.username"
|
||||
:placeholder="loginEnterType"
|
||||
class="input input-lg w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div class="mt-6 relative">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm/6 font-medium text-gray-900 dark:text-gray-300"
|
||||
>{{ t('login.password') }}</label
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
v-model="passwordForm.password"
|
||||
:placeholder="t('login.enterYourPassword')"
|
||||
class="input input-lg w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户协议 -->
|
||||
<div class="mt-5" v-if="globalConfig.isAutoOpenAgreement === '1'">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="agreement-password"
|
||||
v-model="agreedToUserAgreement"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 dark:border-gray-700 dark:bg-gray-800"
|
||||
/>
|
||||
<p class="ml-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
登录即代表同意
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
@click="handleClick"
|
||||
>《{{ globalConfig.agreementTitle }}》</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg w-full rounded-full disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="loading || !passwordForm.username.trim() || !passwordForm.password"
|
||||
>
|
||||
<span v-if="loading" class="inline-block mr-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
</span>
|
||||
{{ t('login.loginAccount') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 验证码登录表单 -->
|
||||
<form
|
||||
v-if="loginMode === 'captcha'"
|
||||
ref="formRef"
|
||||
class="flex flex-col flex-1 justify-between"
|
||||
@submit="handlerSubmit"
|
||||
>
|
||||
<div>
|
||||
<!-- 联系方式输入框 -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
for="contact"
|
||||
class="block text-sm/6 font-medium text-gray-900 dark:text-gray-300"
|
||||
>{{ loginTypeText }}</label
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
id="contact"
|
||||
type="text"
|
||||
v-model="captchaForm.contact"
|
||||
:placeholder="t('login.enterContact') + loginTypeText"
|
||||
class="input input-lg w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码输入框 -->
|
||||
<div class="mt-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
for="captchaId"
|
||||
class="block text-sm/6 font-medium text-gray-900 dark:text-gray-300"
|
||||
>验证码</label
|
||||
>
|
||||
<div class="relative px-1">
|
||||
<div class="flex relative">
|
||||
<input
|
||||
id="captchaId"
|
||||
type="text"
|
||||
v-model="captchaForm.captchaId"
|
||||
:placeholder="t('login.enterCode')"
|
||||
class="input input-lg w-full pr-32"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-captcha px-4"
|
||||
:disabled="loading || lastSendPhoneCodeTime > 0 || !captchaForm.contact.trim()"
|
||||
@click="isShow = true"
|
||||
>
|
||||
<span v-if="loading && lastSendPhoneCodeTime === 0" class="inline-block mr-1">
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-current"></div>
|
||||
</span>
|
||||
{{
|
||||
lastSendPhoneCodeTime > 0
|
||||
? `${lastSendPhoneCodeTime}秒`
|
||||
: t('login.sendVerificationCode')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码组件 -->
|
||||
<div class="rounded-lg">
|
||||
<SliderCaptcha
|
||||
:show="isShow"
|
||||
@success="handleSendCaptcha()"
|
||||
@close="isShow = false"
|
||||
class="z-[10000]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 用户协议 -->
|
||||
<div class="mt-5" v-if="globalConfig.isAutoOpenAgreement === '1'">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="agreement-captcha"
|
||||
v-model="agreedToUserAgreement"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 dark:border-gray-700 dark:bg-gray-800"
|
||||
/>
|
||||
<p class="ml-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
登录即代表同意
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
@click="handleClick"
|
||||
>《{{ globalConfig.agreementTitle }}》</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg w-full rounded-full disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="loading || !captchaForm.contact.trim() || !captchaForm.captchaId"
|
||||
>
|
||||
<span v-if="loading" class="inline-block mr-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
</span>
|
||||
验证码登录
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
109
chat/src/components/Login/Login.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { Close } from '@icon-park/vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Email from './Email.vue'
|
||||
import Wechat from './Wechat.vue'
|
||||
|
||||
defineProps<Props>()
|
||||
const authStore = useAuthStore()
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
// 当前登录类型:wechat(微信登录), password(密码登录), captcha(验证码登录)
|
||||
const loginType = ref('wechat')
|
||||
|
||||
const emailLoginStatus = computed(() => Number(authStore.globalConfig.emailLoginStatus) === 1)
|
||||
const wechatRegisterStatus = computed(
|
||||
() => Number(authStore.globalConfig.wechatRegisterStatus) === 1
|
||||
)
|
||||
const phoneLoginStatus = computed(() => Number(authStore.globalConfig.phoneLoginStatus) === 1)
|
||||
|
||||
// 自动选择合适的登录方式
|
||||
onMounted(() => {
|
||||
setDefaultLoginType()
|
||||
})
|
||||
|
||||
// 根据可用的登录方式设置默认登录类型
|
||||
function setDefaultLoginType() {
|
||||
if (wechatRegisterStatus.value) {
|
||||
loginType.value = 'wechat'
|
||||
} else if (emailLoginStatus.value || phoneLoginStatus.value) {
|
||||
loginType.value = 'captcha'
|
||||
} else {
|
||||
loginType.value = 'password'
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
/* 切换登录类型 */
|
||||
function changeLoginType(type: string) {
|
||||
loginType.value = type
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black bg-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white py-12 rounded-xl shadow-lg w-full h-[32rem] flex flex-col dark:bg-gray-900 dark:text-gray-300 relative"
|
||||
:class="{ 'w-[98vw] px-4': isMobile, 'max-w-xl px-8': !isMobile }"
|
||||
>
|
||||
<button
|
||||
@click="authStore.setLoginDialog(false)"
|
||||
class="btn-icon btn-sm absolute top-4 right-4 z-30 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
<Close theme="outline" size="18" />
|
||||
</button>
|
||||
|
||||
<div class="flex-1 flex flex-col items-center justify-center">
|
||||
<!-- 登录类型切换栏 -->
|
||||
<div
|
||||
class="w-full flex justify-center mb-10"
|
||||
:class="{ 'px-5': isMobile, 'px-10': !isMobile }"
|
||||
>
|
||||
<div class="tab-group tab-group-default dark:bg-gray-800">
|
||||
<button
|
||||
v-if="wechatRegisterStatus"
|
||||
@click="changeLoginType('wechat')"
|
||||
class="tab tab-lg"
|
||||
:class="{ 'tab-active': loginType === 'wechat', 'px-0': isMobile }"
|
||||
>
|
||||
微信登录
|
||||
</button>
|
||||
<button
|
||||
v-if="emailLoginStatus || phoneLoginStatus"
|
||||
@click="changeLoginType('captcha')"
|
||||
class="tab tab-lg"
|
||||
:class="{ 'tab-active': loginType === 'captcha', 'px-0': isMobile }"
|
||||
>
|
||||
验证码登录
|
||||
</button>
|
||||
<button
|
||||
@click="changeLoginType('password')"
|
||||
class="tab tab-lg"
|
||||
:class="{ 'tab-active': loginType === 'password', 'px-0': isMobile }"
|
||||
>
|
||||
密码登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录组件区域 -->
|
||||
<div class="w-full flex-1 flex flex-col overflow-hidden">
|
||||
<Wechat v-if="loginType === 'wechat'" @changeLoginType="changeLoginType" />
|
||||
<Email
|
||||
v-else
|
||||
:login-mode="loginType === 'password' ? 'password' : 'captcha'"
|
||||
@changeLoginType="changeLoginType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
819
chat/src/components/Login/SliderCaptcha.vue
Normal file
@@ -0,0 +1,819 @@
|
||||
<template>
|
||||
<!-- 本体部分 -->
|
||||
<div
|
||||
:class="['vue-puzzle-vcode', { show_: show }]"
|
||||
@mousedown="onCloseMouseDown"
|
||||
@mouseup="onCloseMouseUp"
|
||||
@touchstart="onCloseMouseDown"
|
||||
@touchend="onCloseMouseUp"
|
||||
>
|
||||
<div
|
||||
class="vue-auth-box_ rounded-lg bg-white dark:bg-gray-800"
|
||||
@mousedown.stop
|
||||
@touchstart.stop
|
||||
>
|
||||
<div class="auth-body_" :style="`height: ${canvasHeight}px`">
|
||||
<!-- 主图,有缺口 -->
|
||||
<canvas
|
||||
ref="canvas1"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
:style="`width:${canvasWidth}px;height:${canvasHeight}px`"
|
||||
/>
|
||||
<!-- 成功后显示的完整图 -->
|
||||
<canvas
|
||||
ref="canvas3"
|
||||
:class="['auth-canvas3_', { show: isSuccess }]"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
:style="`width:${canvasWidth}px;height:${canvasHeight}px`"
|
||||
/>
|
||||
<!-- 小图 -->
|
||||
<canvas
|
||||
:width="puzzleBaseSize"
|
||||
class="auth-canvas2_"
|
||||
:height="canvasHeight"
|
||||
ref="canvas2"
|
||||
:style="`width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${
|
||||
styleWidth -
|
||||
sliderBaseSize -
|
||||
(puzzleBaseSize - sliderBaseSize) *
|
||||
((styleWidth - sliderBaseSize) / (canvasWidth - sliderBaseSize))
|
||||
}px)`"
|
||||
/>
|
||||
<div :class="['loading-box_', { hide_: !loading }]">
|
||||
<div class="loading-gif_">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['info-box_', { show: infoBoxShow }, { fail: infoBoxFail }]">
|
||||
{{ infoText }}
|
||||
</div>
|
||||
<div
|
||||
:class="['flash_', { show: isSuccess }]"
|
||||
:style="`transform: translateX(${
|
||||
isSuccess ? `${canvasWidth + canvasHeight * 0.578}px` : `-${canvasHeight * 0.578}px`
|
||||
}) skew(-30deg, 0);`"
|
||||
></div>
|
||||
<img class="reset_" @click="reset" :src="resetSvg" />
|
||||
</div>
|
||||
<div class="auth-control_">
|
||||
<div class="range-box bg-gray-100 dark:bg-gray-700" :style="`height:${sliderBaseSize}px`">
|
||||
<div class="range-text">{{ sliderText }}</div>
|
||||
<div class="range-slider" ref="range-slider" :style="`width:${styleWidth}px`">
|
||||
<div
|
||||
:class="['range-btn', { isDown: mouseDown }]"
|
||||
:style="`width:${sliderBaseSize}px`"
|
||||
@mousedown="onRangeMouseDown($event)"
|
||||
@touchstart="onRangeMouseDown($event)"
|
||||
>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import resetSvg from '@/assets/reset.png'
|
||||
export default {
|
||||
props: {
|
||||
canvasWidth: { type: Number, default: 310 }, // 主canvas的宽
|
||||
canvasHeight: { type: Number, default: 160 }, // 主canvas的高
|
||||
// 是否出现,由父级控制
|
||||
show: { type: Boolean, default: false },
|
||||
puzzleScale: { type: Number, default: 1 }, // 拼图块的大小缩放比例
|
||||
sliderSize: { type: Number, default: 40 }, // 滑块的大小
|
||||
range: { type: Number, default: 10 }, // 允许的偏差值
|
||||
// 所有的背景图片
|
||||
imgs: {
|
||||
type: Array,
|
||||
},
|
||||
successText: {
|
||||
type: String,
|
||||
default: '验证通过!',
|
||||
},
|
||||
failText: {
|
||||
type: String,
|
||||
default: '验证失败,请重试',
|
||||
},
|
||||
sliderText: {
|
||||
type: String,
|
||||
default: '拖动滑块完成拼图',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
mouseDown: false, // 鼠标是否在按钮上按下
|
||||
startWidth: 50, // 鼠标点下去时父级的width
|
||||
startX: 0, // 鼠标按下时的X
|
||||
newX: 0, // 鼠标当前的偏移X
|
||||
pinX: 0, // 拼图的起始X
|
||||
pinY: 0, // 拼图的起始Y
|
||||
loading: false, // 是否正在加在中,主要是等图片onload
|
||||
isCanSlide: false, // 是否可以拉动滑动条
|
||||
error: false, // 图片加在失败会出现这个,提示用户手动刷新
|
||||
infoBoxShow: false, // 提示信息是否出现
|
||||
infoText: '', // 提示等信息
|
||||
infoBoxFail: false, // 是否验证失败
|
||||
timer1: null, // setTimout1
|
||||
closeDown: false, // 为了解决Mac上的click BUG
|
||||
isSuccess: false, // 验证成功
|
||||
imgIndex: -1, // 用于自定义图片时不会随机到重复的图片
|
||||
isSubmting: false, // 是否正在判定,主要用于判定中不能点击重置按钮
|
||||
resetSvg,
|
||||
}
|
||||
},
|
||||
|
||||
/** 生命周期 **/
|
||||
mounted() {
|
||||
document.body.appendChild(this.$el)
|
||||
document.addEventListener('mousemove', this.onRangeMouseMove, false)
|
||||
document.addEventListener('mouseup', this.onRangeMouseUp, false)
|
||||
|
||||
document.addEventListener('touchmove', this.onRangeMouseMove, {
|
||||
passive: false,
|
||||
})
|
||||
document.addEventListener('touchend', this.onRangeMouseUp, false)
|
||||
if (this.show) {
|
||||
document.body.classList.add('vue-puzzle-overflow')
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timer1)
|
||||
document.body.removeChild(this.$el)
|
||||
document.removeEventListener('mousemove', this.onRangeMouseMove, false)
|
||||
document.removeEventListener('mouseup', this.onRangeMouseUp, false)
|
||||
|
||||
document.removeEventListener('touchmove', this.onRangeMouseMove, {
|
||||
passive: false,
|
||||
})
|
||||
document.removeEventListener('touchend', this.onRangeMouseUp, false)
|
||||
},
|
||||
|
||||
/** 监听 **/
|
||||
watch: {
|
||||
show(newV) {
|
||||
// 每次出现都应该重新初始化
|
||||
if (newV) {
|
||||
document.body.classList.add('vue-puzzle-overflow')
|
||||
this.reset()
|
||||
} else {
|
||||
this.isSubmting = false
|
||||
this.isSuccess = false
|
||||
this.infoBoxShow = false
|
||||
document.body.classList.remove('vue-puzzle-overflow')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/** 计算属性 **/
|
||||
computed: {
|
||||
// styleWidth是底部用户操作的滑块的父级,就是轨道在鼠标的作用下应该具有的宽度
|
||||
styleWidth() {
|
||||
const w = this.startWidth + this.newX - this.startX
|
||||
return w < this.sliderBaseSize
|
||||
? this.sliderBaseSize
|
||||
: w > this.canvasWidth
|
||||
? this.canvasWidth
|
||||
: w
|
||||
},
|
||||
// 图中拼图块的60 * 用户设定的缩放比例计算之后的值 0.2~2
|
||||
puzzleBaseSize() {
|
||||
return Math.round(Math.max(Math.min(this.puzzleScale, 2), 0.2) * 52.5 + 6)
|
||||
},
|
||||
// 处理一下sliderSize,弄成整数,以免计算有偏差
|
||||
sliderBaseSize() {
|
||||
return Math.max(Math.min(Math.round(this.sliderSize), Math.round(this.canvasWidth * 0.5)), 10)
|
||||
},
|
||||
},
|
||||
|
||||
/** 方法 **/
|
||||
methods: {
|
||||
// 关闭
|
||||
onClose() {
|
||||
if (!this.mouseDown && !this.isSubmting) {
|
||||
clearTimeout(this.timer1)
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
onCloseMouseDown() {
|
||||
this.closeDown = true
|
||||
},
|
||||
onCloseMouseUp() {
|
||||
if (this.closeDown) {
|
||||
this.onClose()
|
||||
}
|
||||
this.closeDown = false
|
||||
},
|
||||
// 鼠标按下准备拖动
|
||||
onRangeMouseDown(e) {
|
||||
if (this.isCanSlide) {
|
||||
this.mouseDown = true
|
||||
this.startWidth = this.$refs['range-slider'].clientWidth
|
||||
this.newX = e.clientX || e.changedTouches[0].clientX
|
||||
this.startX = e.clientX || e.changedTouches[0].clientX
|
||||
}
|
||||
},
|
||||
// 鼠标移动
|
||||
onRangeMouseMove(e) {
|
||||
if (this.mouseDown) {
|
||||
e.preventDefault()
|
||||
this.newX = e.clientX || e.changedTouches[0].clientX
|
||||
}
|
||||
},
|
||||
// 鼠标抬起
|
||||
onRangeMouseUp() {
|
||||
if (this.mouseDown) {
|
||||
this.mouseDown = false
|
||||
this.submit()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 开始进行
|
||||
* @param withCanvas 是否强制使用canvas随机作图
|
||||
*/
|
||||
init(withCanvas) {
|
||||
// 防止重复加载导致的渲染错误
|
||||
if (this.loading && !withCanvas) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.isCanSlide = false
|
||||
const c = this.$refs.canvas1
|
||||
const c2 = this.$refs.canvas2
|
||||
const c3 = this.$refs.canvas3
|
||||
const ctx = c.getContext('2d')
|
||||
const ctx2 = c2.getContext('2d')
|
||||
const ctx3 = c3.getContext('2d')
|
||||
const isFirefox =
|
||||
navigator.userAgent.indexOf('Firefox') >= 0 && navigator.userAgent.indexOf('Windows') >= 0 // 是windows版火狐
|
||||
const img = document.createElement('img')
|
||||
ctx.fillStyle = 'rgba(255,255,255,1)'
|
||||
ctx3.fillStyle = 'rgba(255,255,255,1)'
|
||||
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
||||
ctx2.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
||||
|
||||
// 取一个随机坐标,作为拼图块的位置
|
||||
this.pinX = this.getRandom(this.puzzleBaseSize, this.canvasWidth - this.puzzleBaseSize - 20) // 留20的边距
|
||||
this.pinY = this.getRandom(20, this.canvasHeight - this.puzzleBaseSize - 20) // 主图高度 - 拼图块自身高度 - 20边距
|
||||
img.crossOrigin = 'anonymous' // 匿名,想要获取跨域的图片
|
||||
img.onload = () => {
|
||||
const [x, y, w, h] = this.makeImgSize(img)
|
||||
ctx.save()
|
||||
// 先画小图
|
||||
this.paintBrick(ctx)
|
||||
ctx.closePath()
|
||||
if (!isFirefox) {
|
||||
ctx.shadowOffsetX = 0
|
||||
ctx.shadowOffsetY = 0
|
||||
ctx.shadowColor = '#000'
|
||||
ctx.shadowBlur = 3
|
||||
ctx.fill()
|
||||
ctx.clip()
|
||||
} else {
|
||||
ctx.clip()
|
||||
ctx.save()
|
||||
ctx.shadowOffsetX = 0
|
||||
ctx.shadowOffsetY = 0
|
||||
ctx.shadowColor = '#000'
|
||||
ctx.shadowBlur = 3
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
ctx.drawImage(img, x, y, w, h)
|
||||
ctx3.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
|
||||
ctx3.drawImage(img, x, y, w, h)
|
||||
|
||||
// 设置小图的内阴影
|
||||
ctx.globalCompositeOperation = 'source-atop'
|
||||
|
||||
this.paintBrick(ctx)
|
||||
|
||||
ctx.arc(
|
||||
this.pinX + Math.ceil(this.puzzleBaseSize / 2),
|
||||
this.pinY + Math.ceil(this.puzzleBaseSize / 2),
|
||||
this.puzzleBaseSize * 1.2,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true
|
||||
)
|
||||
ctx.closePath()
|
||||
ctx.shadowColor = 'rgba(255, 255, 255, .8)'
|
||||
ctx.shadowOffsetX = -1
|
||||
ctx.shadowOffsetY = -1
|
||||
ctx.shadowBlur = Math.min(Math.ceil(8 * this.puzzleScale), 12)
|
||||
ctx.fillStyle = '#ffffaa'
|
||||
ctx.fill()
|
||||
|
||||
// 将小图赋值给ctx2
|
||||
const imgData = ctx.getImageData(
|
||||
this.pinX - 3, // 为了阴影 是从-3px开始截取,判定的时候要+3px
|
||||
this.pinY - 20,
|
||||
this.pinX + this.puzzleBaseSize + 5,
|
||||
this.pinY + this.puzzleBaseSize + 5
|
||||
)
|
||||
ctx2.putImageData(imgData, 0, this.pinY - 20)
|
||||
|
||||
// ctx2.drawImage(c, this.pinX - 3,this.pinY - 20,this.pinX + this.puzzleBaseSize + 5,this.pinY + this.puzzleBaseSize + 5,
|
||||
// 0, this.pinY - 20, this.pinX + this.puzzleBaseSize + 5, this.pinY + this.puzzleBaseSize + 5);
|
||||
|
||||
// 清理
|
||||
ctx.restore()
|
||||
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
||||
|
||||
// 画缺口
|
||||
ctx.save()
|
||||
this.paintBrick(ctx)
|
||||
ctx.globalAlpha = 0.8
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
|
||||
// 画缺口的内阴影
|
||||
ctx.save()
|
||||
ctx.globalCompositeOperation = 'source-atop'
|
||||
this.paintBrick(ctx)
|
||||
ctx.arc(
|
||||
this.pinX + Math.ceil(this.puzzleBaseSize / 2),
|
||||
this.pinY + Math.ceil(this.puzzleBaseSize / 2),
|
||||
this.puzzleBaseSize * 1.2,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true
|
||||
)
|
||||
ctx.shadowColor = '#000'
|
||||
ctx.shadowOffsetX = 2
|
||||
ctx.shadowOffsetY = 2
|
||||
ctx.shadowBlur = 16
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
|
||||
// 画整体背景图
|
||||
ctx.save()
|
||||
ctx.globalCompositeOperation = 'destination-over'
|
||||
ctx.drawImage(img, x, y, w, h)
|
||||
ctx.restore()
|
||||
|
||||
this.loading = false
|
||||
this.isCanSlide = true
|
||||
}
|
||||
img.onerror = () => {
|
||||
this.init(true) // 如果图片加载错误就重新来,并强制用canvas随机作图
|
||||
}
|
||||
|
||||
if (!withCanvas && this.imgs && this.imgs.length) {
|
||||
let randomNum = this.getRandom(0, this.imgs.length - 1)
|
||||
if (randomNum === this.imgIndex) {
|
||||
if (randomNum === this.imgs.length - 1) {
|
||||
randomNum = 0
|
||||
} else {
|
||||
randomNum++
|
||||
}
|
||||
}
|
||||
this.imgIndex = randomNum
|
||||
img.src = this.imgs[randomNum]
|
||||
} else {
|
||||
img.src = this.makeImgWithCanvas()
|
||||
}
|
||||
},
|
||||
// 工具 - 范围随机数
|
||||
getRandom(min, max) {
|
||||
return Math.ceil(Math.random() * (max - min) + min)
|
||||
},
|
||||
// 工具 - 设置图片尺寸cover方式贴合canvas尺寸 w/h
|
||||
makeImgSize(img) {
|
||||
const imgScale = img.width / img.height
|
||||
const canvasScale = this.canvasWidth / this.canvasHeight
|
||||
let x = 0,
|
||||
y = 0,
|
||||
w = 0,
|
||||
h = 0
|
||||
if (imgScale > canvasScale) {
|
||||
h = this.canvasHeight
|
||||
w = imgScale * h
|
||||
y = 0
|
||||
x = (this.canvasWidth - w) / 2
|
||||
} else {
|
||||
w = this.canvasWidth
|
||||
h = w / imgScale
|
||||
x = 0
|
||||
y = (this.canvasHeight - h) / 2
|
||||
}
|
||||
return [x, y, w, h]
|
||||
},
|
||||
// 绘制拼图块的路径
|
||||
paintBrick(ctx) {
|
||||
const moveL = Math.ceil(15 * this.puzzleScale) // 直线移动的基础距离
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(this.pinX, this.pinY)
|
||||
ctx.lineTo(this.pinX + moveL, this.pinY)
|
||||
ctx.arcTo(
|
||||
this.pinX + moveL,
|
||||
this.pinY - moveL / 2,
|
||||
this.pinX + moveL + moveL / 2,
|
||||
this.pinY - moveL / 2,
|
||||
moveL / 2
|
||||
)
|
||||
ctx.arcTo(
|
||||
this.pinX + moveL + moveL,
|
||||
this.pinY - moveL / 2,
|
||||
this.pinX + moveL + moveL,
|
||||
this.pinY,
|
||||
moveL / 2
|
||||
)
|
||||
ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY)
|
||||
ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL)
|
||||
ctx.arcTo(
|
||||
this.pinX + moveL + moveL + moveL + moveL / 2,
|
||||
this.pinY + moveL,
|
||||
this.pinX + moveL + moveL + moveL + moveL / 2,
|
||||
this.pinY + moveL + moveL / 2,
|
||||
moveL / 2
|
||||
)
|
||||
ctx.arcTo(
|
||||
this.pinX + moveL + moveL + moveL + moveL / 2,
|
||||
this.pinY + moveL + moveL,
|
||||
this.pinX + moveL + moveL + moveL,
|
||||
this.pinY + moveL + moveL,
|
||||
moveL / 2
|
||||
)
|
||||
ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL + moveL + moveL)
|
||||
ctx.lineTo(this.pinX, this.pinY + moveL + moveL + moveL)
|
||||
ctx.lineTo(this.pinX, this.pinY + moveL + moveL)
|
||||
|
||||
ctx.arcTo(
|
||||
this.pinX + moveL / 2,
|
||||
this.pinY + moveL + moveL,
|
||||
this.pinX + moveL / 2,
|
||||
this.pinY + moveL + moveL / 2,
|
||||
moveL / 2
|
||||
)
|
||||
ctx.arcTo(this.pinX + moveL / 2, this.pinY + moveL, this.pinX, this.pinY + moveL, moveL / 2)
|
||||
ctx.lineTo(this.pinX, this.pinY)
|
||||
},
|
||||
// 用canvas随机生成图片
|
||||
makeImgWithCanvas() {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
canvas.width = this.canvasWidth
|
||||
canvas.height = this.canvasHeight
|
||||
ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
|
||||
100,
|
||||
255
|
||||
)},${this.getRandom(100, 255)})`
|
||||
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
|
||||
// 随机画10个图形
|
||||
for (let i = 0; i < 12; i++) {
|
||||
ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
|
||||
100,
|
||||
255
|
||||
)},${this.getRandom(100, 255)})`
|
||||
ctx.strokeStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
|
||||
100,
|
||||
255
|
||||
)},${this.getRandom(100, 255)})`
|
||||
|
||||
if (this.getRandom(0, 2) > 1) {
|
||||
// 矩形
|
||||
ctx.save()
|
||||
ctx.rotate((this.getRandom(-90, 90) * Math.PI) / 180)
|
||||
ctx.fillRect(
|
||||
this.getRandom(-20, canvas.width - 20),
|
||||
this.getRandom(-20, canvas.height - 20),
|
||||
this.getRandom(10, canvas.width / 2 + 10),
|
||||
this.getRandom(10, canvas.height / 2 + 10)
|
||||
)
|
||||
ctx.restore()
|
||||
} else {
|
||||
// 圆
|
||||
ctx.beginPath()
|
||||
const ran = this.getRandom(-Math.PI, Math.PI)
|
||||
ctx.arc(
|
||||
this.getRandom(0, canvas.width),
|
||||
this.getRandom(0, canvas.height),
|
||||
this.getRandom(10, canvas.height / 2 + 10),
|
||||
ran,
|
||||
ran + Math.PI * 1.5
|
||||
)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
return canvas.toDataURL('image/png')
|
||||
},
|
||||
// 开始判定
|
||||
submit() {
|
||||
this.isSubmting = true
|
||||
// 偏差 x = puzzle的起始X - (用户真滑动的距离) + (puzzle的宽度 - 滑块的宽度) * (用户真滑动的距离/canvas总宽度)
|
||||
// 最后+ 的是补上slider和滑块宽度不一致造成的缝隙
|
||||
const x = Math.abs(
|
||||
this.pinX -
|
||||
(this.styleWidth - this.sliderBaseSize) +
|
||||
(this.puzzleBaseSize - this.sliderBaseSize) *
|
||||
((this.styleWidth - this.sliderBaseSize) / (this.canvasWidth - this.sliderBaseSize)) -
|
||||
3
|
||||
)
|
||||
if (x < this.range) {
|
||||
// 成功
|
||||
this.infoText = this.successText
|
||||
this.infoBoxFail = false
|
||||
this.infoBoxShow = true
|
||||
this.isCanSlide = false
|
||||
this.isSuccess = true
|
||||
// 成功后准备关闭
|
||||
clearTimeout(this.timer1)
|
||||
this.timer1 = setTimeout(() => {
|
||||
// 成功的回调
|
||||
this.isSubmting = false
|
||||
this.$emit('success', x)
|
||||
}, 800)
|
||||
} else {
|
||||
// 失败
|
||||
this.infoText = this.failText
|
||||
this.infoBoxFail = true
|
||||
this.infoBoxShow = true
|
||||
this.isCanSlide = false
|
||||
// 失败的回调
|
||||
this.$emit('fail', x)
|
||||
// 800ms后重置
|
||||
clearTimeout(this.timer1)
|
||||
this.timer1 = setTimeout(() => {
|
||||
this.isSubmting = false
|
||||
this.reset()
|
||||
}, 800)
|
||||
}
|
||||
},
|
||||
// 重置 - 重新设置初始状态
|
||||
resetState() {
|
||||
this.infoBoxFail = false
|
||||
this.infoBoxShow = false
|
||||
this.isCanSlide = false
|
||||
this.isSuccess = false
|
||||
this.startWidth = this.sliderBaseSize // 鼠标点下去时父级的width
|
||||
this.startX = 0 // 鼠标按下时的X
|
||||
this.newX = 0 // 鼠标当前的偏移X
|
||||
},
|
||||
|
||||
// 重置
|
||||
reset() {
|
||||
if (this.isSubmting) {
|
||||
return
|
||||
}
|
||||
this.resetState()
|
||||
this.init()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.vue-puzzle-vcode {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 200ms;
|
||||
&.show_ {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
.vue-auth-box_ {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
// background: #fff;
|
||||
user-select: none;
|
||||
// border-radius: 3px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
.auth-body_ {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
.loading-box_ {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 20;
|
||||
opacity: 1;
|
||||
transition: opacity 200ms;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&.hide_ {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
.loading-gif_ {
|
||||
span {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
}
|
||||
.loading-gif_ {
|
||||
flex: none;
|
||||
height: 5px;
|
||||
line-height: 0;
|
||||
@keyframes load {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
}
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
margin-left: 2px;
|
||||
border-radius: 50%;
|
||||
background-color: #888;
|
||||
animation: load 1.04s ease infinite;
|
||||
&:nth-child(1) {
|
||||
margin-left: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.13s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.26s;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
animation-delay: 0.39s;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
animation-delay: 0.52s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.info-box_ {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
background-color: #83ce3f;
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: all 200ms;
|
||||
color: #fff;
|
||||
z-index: 10;
|
||||
&.show {
|
||||
opacity: 0.95;
|
||||
transform: translateY(0);
|
||||
}
|
||||
&.fail {
|
||||
background-color: #ce594b;
|
||||
}
|
||||
}
|
||||
.auth-canvas2_ {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
.auth-canvas3_ {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 600ms;
|
||||
z-index: 3;
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.flash_ {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
z-index: 3;
|
||||
&.show {
|
||||
transition: transform 600ms;
|
||||
}
|
||||
}
|
||||
.reset_ {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 35px;
|
||||
height: auto;
|
||||
z-index: 12;
|
||||
cursor: pointer;
|
||||
transition: transform 200ms;
|
||||
transform: rotate(0deg);
|
||||
&:hover {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.auth-control_ {
|
||||
.range-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
// background-color: #eef1f8;
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 8px rgba(240, 240, 240, 0.6) inset;
|
||||
.range-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 14px;
|
||||
color: #b7bcd1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.range-slider {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
background-color: rgba(106, 160, 255, 0.8);
|
||||
border-radius: 3px;
|
||||
.range-btn {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 4px #ccc;
|
||||
cursor: pointer;
|
||||
& > div {
|
||||
width: 0;
|
||||
height: 40%;
|
||||
|
||||
transition: all 200ms;
|
||||
&:nth-child(2) {
|
||||
margin: 0 4px;
|
||||
}
|
||||
border: solid 1px #6aa0ff;
|
||||
}
|
||||
&:hover,
|
||||
&.isDown {
|
||||
& > div:first-child {
|
||||
border: solid 4px transparent;
|
||||
height: 0;
|
||||
border-right-color: #6aa0ff;
|
||||
}
|
||||
& > div:nth-child(2) {
|
||||
border-width: 3px;
|
||||
height: 0;
|
||||
border-radius: 3px;
|
||||
margin: 0 6px;
|
||||
border-right-color: #6aa0ff;
|
||||
}
|
||||
& > div:nth-child(3) {
|
||||
border: solid 4px transparent;
|
||||
height: 0;
|
||||
border-left-color: #6aa0ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.vue-puzzle-overflow {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
159
chat/src/components/Login/Wechat.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import type { ResData } from '@/api/types'
|
||||
import { fetchGetQRCodeAPI, fetchGetQRSceneStrAPI, fetchLoginBySceneStrAPI } from '@/api/user'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { DIALOG_TABS } from '@/store/modules/global'
|
||||
import { message } from '@/utils/message'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const timer = ref()
|
||||
const countdownTimer = ref()
|
||||
const timerStartTime = ref(0)
|
||||
const wxLoginUrl = ref('')
|
||||
const sceneStr = ref('')
|
||||
const activeCount = ref(false)
|
||||
const loading = ref(true) // 控制加载状态
|
||||
const ms = message()
|
||||
const authStore = useAuthStore()
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const agreedToUserAgreement = ref(true) // 读取初始状态并转换为布尔类型
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
|
||||
// 点击"用户协议及隐私政策"时,自动同意
|
||||
function handleClick() {
|
||||
agreedToUserAgreement.value = true // 设置为同意
|
||||
useGlobalStore.updateSettingsDialog(true, DIALOG_TABS.AGREEMENT)
|
||||
}
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
|
||||
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 = {}
|
||||
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.value)
|
||||
ms.success(t('login.loginSuccess'))
|
||||
authStore.setToken(res.data)
|
||||
authStore.getUserInfo()
|
||||
authStore.setLoginDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function getQrCodeUrl() {
|
||||
loading.value = true // 开始加载
|
||||
const res: ResData = await fetchGetQRCodeAPI({ sceneStr: sceneStr.value })
|
||||
if (res.success) {
|
||||
activeCount.value = true
|
||||
await loadImage(res.data)
|
||||
wxLoginUrl.value = res.data
|
||||
loading.value = false // 加载完成
|
||||
timerStartTime.value = Date.now()
|
||||
timer.value = setInterval(() => {
|
||||
if (Date.now() - timerStartTime.value > 60000) {
|
||||
clearInterval(timer.value)
|
||||
return
|
||||
}
|
||||
loginBySnece()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimeDown() {
|
||||
// clearInterval(timer.value);
|
||||
getSeneStr()
|
||||
// 重新获取二维码无需依赖 countdownRef
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleTimeDown()
|
||||
if (countdownTimer.value !== null) {
|
||||
clearInterval(countdownTimer.value)
|
||||
}
|
||||
countdownTimer.value = setInterval(handleTimeDown, 60000)
|
||||
// getSeneStr();
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清除用于检测的timer
|
||||
if (timer.value !== null) {
|
||||
clearInterval(timer.value)
|
||||
}
|
||||
// 组件卸载时,也清除handleTimeDown的countdownTimer
|
||||
if (countdownTimer.value !== null) {
|
||||
clearInterval(countdownTimer.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col justify-between" :class="isMobile ? 'px-5 ' : 'px-10 '">
|
||||
<div class="flex flex-col items-center flex-1">
|
||||
<div class="relative w-[200px] h-[200px] mb-6 mt-auto">
|
||||
<img
|
||||
v-if="wxLoginUrl && (agreedToUserAgreement || globalConfig.isAutoOpenAgreement !== '1')"
|
||||
class="w-full h-full select-none shadow-sm rounded-lg object-cover border border-gray-100 dark:border-gray-700"
|
||||
:src="wxLoginUrl"
|
||||
alt="微信登录二维码"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full rounded-lg bg-gray-200 dark:bg-gray-700 animate-pulse"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<div
|
||||
class="animate-spin rounded-full h-10 w-10 border-b-2 border-primary-600 dark:border-primary-400"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">请使用微信扫描二维码登录</p>
|
||||
|
||||
<div v-if="globalConfig.isAutoOpenAgreement === '1'" class="flex items-center mt-2">
|
||||
<input
|
||||
v-model="agreedToUserAgreement"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 dark:border-gray-700 dark:bg-gray-800"
|
||||
/>
|
||||
<p class="ml-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
扫码登录即代表同意
|
||||
<a
|
||||
href="#"
|
||||
class="font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
@click="handleClick"
|
||||
>《用户协议及隐私协议》</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加空白div保持与Email组件对齐 -->
|
||||
<div class="h-6"></div>
|
||||
</div>
|
||||
</template>
|
||||
89
chat/src/components/Message/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<TransitionGroup
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="scale-100 opacity-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
class="fixed top-8 left-1/2 -translate-x-1/2 z-[999999] flex items-center px-4 py-2 rounded-lg shadow-sm overflow-hidden whitespace-nowrap"
|
||||
:class="{
|
||||
'bg-emerald-50 dark:bg-emerald-500/10': msg.type === 'success',
|
||||
'bg-red-50 dark:bg-red-500/10': msg.type === 'error',
|
||||
'bg-yellow-50 dark:bg-yellow-500/10': msg.type === 'warning',
|
||||
'bg-blue-50 dark:bg-blue-500/10': msg.type === 'info',
|
||||
'max-w-[70vw]': isMobile,
|
||||
'max-w-[40vw]': !isMobile,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<CheckOne
|
||||
v-if="msg.type === 'success'"
|
||||
theme="filled"
|
||||
size="20"
|
||||
class="text-emerald-500 dark:text-emerald-400 flex-shrink-0"
|
||||
/>
|
||||
<CloseOne
|
||||
v-if="msg.type === 'error'"
|
||||
theme="filled"
|
||||
size="20"
|
||||
class="text-red-500 dark:text-red-400 flex-shrink-0"
|
||||
/>
|
||||
<Attention
|
||||
v-if="msg.type === 'warning'"
|
||||
theme="filled"
|
||||
size="20"
|
||||
class="text-yellow-500 dark:text-yellow-400 flex-shrink-0"
|
||||
/>
|
||||
<Info
|
||||
v-if="msg.type === 'info'"
|
||||
theme="filled"
|
||||
size="20"
|
||||
class="text-blue-500 dark:text-blue-400 flex-shrink-0"
|
||||
/>
|
||||
<span class="text-gray-900 dark:text-gray-100 truncate">{{ msg.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import type { MessageOptions } from '@/utils/message'
|
||||
import { Attention, CheckOne, CloseOne, Info } from '@icon-park/vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Message extends MessageOptions {
|
||||
id: number
|
||||
}
|
||||
|
||||
const messages = ref<Message[]>([])
|
||||
let messageId = 0
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const show = (options: MessageOptions) => {
|
||||
const id = messageId++
|
||||
const msg = {
|
||||
id,
|
||||
type: options.type || 'info',
|
||||
content: options.content,
|
||||
}
|
||||
|
||||
messages.value.push(msg)
|
||||
|
||||
setTimeout(() => {
|
||||
messages.value = messages.value.filter(m => m.id !== id)
|
||||
}, options.duration || 3000)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
228
chat/src/components/MobileSettingsDialog.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-if="props.visible"
|
||||
class="fixed inset-0 z-[9000] flex items-center justify-center bg-gray-900 bg-opacity-50"
|
||||
>
|
||||
<div class="w-full h-full bg-white dark:bg-gray-750 flex flex-col overflow-hidden">
|
||||
<!-- 标题部分 -->
|
||||
<div
|
||||
class="flex justify-between items-center mb-2 flex-shrink-0 px-4 pt-4 pb-2 border-b dark:border-gray-600"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
v-if="currentView !== 'main'"
|
||||
@click="backToMainView"
|
||||
class="mr-2 p-1 rounded-full text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
||||
>
|
||||
<ArrowLeft size="20" />
|
||||
</button>
|
||||
<span class="text-xl font-semibold dark:text-white">
|
||||
{{ currentView === 'main' ? '设置' : currentTabTitle }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="p-1 rounded-full text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
||||
>
|
||||
<Close size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 主体部分 -->
|
||||
<div class="flex flex-col flex-grow overflow-y-auto px-3 pb-4">
|
||||
<!-- 主菜单页面 -->
|
||||
<div v-if="currentView === 'main'" class="flex-grow py-2">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="`mobile-tab-${index}`"
|
||||
class="mb-1 border-b dark:border-gray-600 last:border-b-0"
|
||||
>
|
||||
<div
|
||||
@click="navigateToTab(index)"
|
||||
class="flex justify-between items-center px-4 py-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors duration-150"
|
||||
:class="{
|
||||
'text-gray-800 dark:text-gray-200': true,
|
||||
}"
|
||||
>
|
||||
<span class="font-medium text-base">{{ tab.name }}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二级页面内容 -->
|
||||
<div v-else class="flex-grow py-2">
|
||||
<keep-alive>
|
||||
<component
|
||||
v-if="tabs[currentViewIndex]?.component"
|
||||
:is="tabs[currentViewIndex].component"
|
||||
:key="`mobile-component-${currentViewIndex}-${activeKey}`"
|
||||
:visible="props.visible && currentView !== 'main'"
|
||||
></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
|
||||
<!-- 退出登录按钮 (只在主页面显示) -->
|
||||
<div v-if="currentView === 'main'" class="mt-auto pt-4 pb-2 flex-shrink-0 px-4">
|
||||
<button
|
||||
@click="showLogoutConfirmation"
|
||||
class="w-full flex items-center justify-center gap-3 px-4 py-3 rounded-lg cursor-pointer group font-medium text-sm text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900/30 hover:bg-red-100 dark:hover:bg-red-900/50 border border-red-200 dark:border-red-500/50 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-red-300 dark:focus:ring-red-600"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore, useGlobalStore } from '@/store'
|
||||
import { dialog } from '@/utils/dialog'
|
||||
import { ArrowLeft, Close } from '@icon-park/vue-next'
|
||||
import { computed, markRaw, ref, watch } from 'vue'
|
||||
// Import setting components directly
|
||||
import AccountManagement from './Settings/AccountManagement.vue'
|
||||
import MemberCenter from './Settings/MemberCenter.vue'
|
||||
import NoticeDialog from './Settings/NoticeDialog.vue'
|
||||
import UserAgreement from './Settings/UserAgreement.vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const globalStore = useGlobalStore()
|
||||
const authStore = useAuthStore()
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
|
||||
// 通过计算属性获取初始标签页
|
||||
const initialTab = computed(() => globalStore.mobileInitialTab)
|
||||
|
||||
// 使用computed让tabs内容随条件变化
|
||||
const tabs = computed(() => {
|
||||
const baseTabs = [
|
||||
{ name: '账户管理', component: markRaw(AccountManagement), id: 'account' },
|
||||
{ name: '会员中心', component: markRaw(MemberCenter), id: 'member' },
|
||||
// { name: '数据管理', component: markRaw(DataManagement), id: 'data' },
|
||||
{ name: '网站公告', component: markRaw(NoticeDialog), id: 'notice' },
|
||||
]
|
||||
|
||||
// 只有当 globalConfig.isAutoOpenAgreement === '1' 时才添加用户协议选项
|
||||
if (globalConfig.value.isAutoOpenAgreement === '1') {
|
||||
baseTabs.push({ name: '用户协议', component: markRaw(UserAgreement), id: 'agreement' })
|
||||
}
|
||||
|
||||
return baseTabs
|
||||
})
|
||||
|
||||
// 页面导航状态
|
||||
const currentView = ref('main') // 'main' 或 'tab'
|
||||
const currentViewIndex = ref(-1) // 当前查看的tab索引
|
||||
const activeKey = ref(Date.now())
|
||||
|
||||
// 当前选中的tab标题
|
||||
const currentTabTitle = computed(() => {
|
||||
return currentViewIndex.value >= 0 ? tabs.value[currentViewIndex.value].name : '设置'
|
||||
})
|
||||
|
||||
// 导航到特定Tab
|
||||
function navigateToTab(index: number) {
|
||||
if (index < 0 || index >= tabs.value.length) return
|
||||
|
||||
currentViewIndex.value = index
|
||||
currentView.value = 'tab'
|
||||
activeKey.value = Date.now() // 强制刷新组件
|
||||
}
|
||||
|
||||
// 根据ID导航到特定Tab
|
||||
function navigateToTabById(tabId: string) {
|
||||
const index = tabs.value.findIndex(tab => tab.id === tabId)
|
||||
if (index !== -1) {
|
||||
navigateToTab(index)
|
||||
}
|
||||
}
|
||||
|
||||
// 返回主视图
|
||||
function backToMainView() {
|
||||
currentView.value = 'main'
|
||||
currentViewIndex.value = -1
|
||||
}
|
||||
|
||||
// Close Handler
|
||||
function handleClose() {
|
||||
globalStore.updateMobileSettingsDialog(false)
|
||||
// 重置为主视图,以便下次打开时从主视图开始
|
||||
setTimeout(() => {
|
||||
backToMainView()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 监听visible和initialTab变化
|
||||
watch(
|
||||
[() => props.visible, initialTab],
|
||||
([isVisible, tabId]) => {
|
||||
if (isVisible && tabId) {
|
||||
navigateToTabById(tabId)
|
||||
} else if (!isVisible) {
|
||||
// 当对话框关闭时,延迟重置视图,以便关闭动画完成后不会看到视图突然变化
|
||||
setTimeout(() => {
|
||||
backToMainView()
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Logout Handler
|
||||
function showLogoutConfirmation() {
|
||||
const dialogInstance = dialog()
|
||||
dialogInstance.warning({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
authStore.logOut()
|
||||
handleClose() // Close settings after logout
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Modal fade transition */
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 页面切换过渡 */
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
.page-enter-from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.page-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
</style>
|
||||
284
chat/src/components/PhoneIdentity.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<script setup lang="ts">
|
||||
import { fetchSendSms } from '@/api'
|
||||
import { fetchVerifyPhoneIdentityAPI } from '@/api/user'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { message } from '@/utils/message'
|
||||
import { Close } from '@icon-park/vue-next'
|
||||
import { computed, ref } from 'vue'
|
||||
import SliderCaptcha from './Login/SliderCaptcha.vue'
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const ms = message()
|
||||
const { isMobile } = useBasicLayout()
|
||||
const isShow = ref(false)
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const loading = ref(false)
|
||||
const formRef = ref<HTMLFormElement | null>(null)
|
||||
const lastSendPhoneCodeTime = ref(0)
|
||||
const authStore = useAuthStore()
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
|
||||
const identityForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
phone: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{
|
||||
pattern: /^1[3456789]\d{9}$/,
|
||||
message: '手机号格式错误',
|
||||
},
|
||||
],
|
||||
code: [{ required: true, message: '请输入验证码' }],
|
||||
username: [{ required: true, message: '请输入用户名' }],
|
||||
password: [{ required: true, message: '请输入密码' }],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入密码' },
|
||||
{
|
||||
validator(rule: any, value: string) {
|
||||
if (value !== identityForm.value.password) {
|
||||
return new Error('两次输入的密码不一致')
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 使用 ref 来管理全局参数的状态
|
||||
const agreedToUserAgreement = ref(true) // 读取初始状态并转换为布尔类型
|
||||
|
||||
// 点击"用户协议及隐私政策"时,自动同意
|
||||
function handleClick() {
|
||||
agreedToUserAgreement.value = true // 设置为同意
|
||||
useGlobalStore.updateUserAgreementDialog(true)
|
||||
}
|
||||
|
||||
function handlerSubmit() {
|
||||
if (agreedToUserAgreement.value === false && globalConfig.value.isAutoOpenAgreement === '1') {
|
||||
return ms.error(`请阅读并同意《${globalConfig.value.agreementTitle}》`)
|
||||
}
|
||||
isShow.value = false
|
||||
fetchVerifyPhoneIdentityAPI(identityForm.value).then((res: any) => {
|
||||
if (res.code === 200) {
|
||||
ms.success('认证成功')
|
||||
useGlobalStore.updatePhoneDialog(false)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 发送验证码 */
|
||||
async function handleSendCaptcha() {
|
||||
isShow.value = false
|
||||
// 手动验证表单
|
||||
const { phone } = identityForm.value
|
||||
|
||||
if (!phone) {
|
||||
ms.error('请输入手机号')
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^1[3456789]\d{9}$/.test(phone)) {
|
||||
ms.error('手机号格式错误')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const params: any = { phone }
|
||||
let res: any
|
||||
res = await fetchSendSms(params)
|
||||
const { success, message } = res
|
||||
if (success) {
|
||||
ms.success(res.data)
|
||||
// 记录重新发送倒计时
|
||||
lastSendPhoneCodeTime.value = 60
|
||||
changeLastSendPhoneCodeTime()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 定时器改变倒计时时间方法
|
||||
function changeLastSendPhoneCodeTime() {
|
||||
if (lastSendPhoneCodeTime.value > 0) {
|
||||
setTimeout(() => {
|
||||
lastSendPhoneCodeTime.value--
|
||||
changeLastSendPhoneCodeTime()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black bg-opacity-50 py-6"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded-lg shadow-lg w-full max-h-[70vh] flex flex-col dark:bg-gray-900 dark:text-gray-400 relative"
|
||||
:class="{ 'max-w-[95vw]': isMobile, 'max-w-xl': !isMobile }"
|
||||
>
|
||||
<Close
|
||||
size="18"
|
||||
class="absolute top-3 right-3 cursor-pointer z-30"
|
||||
@click="useGlobalStore.updatePhoneDialog(false)"
|
||||
/>
|
||||
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div
|
||||
class="flex w-full flex-col h-full justify-center"
|
||||
:class="isMobile ? 'px-5 py-5' : 'px-10 py-5'"
|
||||
>
|
||||
<form
|
||||
ref="formRef"
|
||||
:model="identityForm"
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<h2
|
||||
class="mb-8 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
手机号绑定
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex">
|
||||
<input
|
||||
id="userPhone"
|
||||
type="text"
|
||||
v-model="identityForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
class="flex-1 block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 relative">
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
v-model="identityForm.code"
|
||||
placeholder="请输入验证码"
|
||||
class="block w-full rounded-md border-0 py-2 px-2 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400 pl-3 pr-12"
|
||||
/>
|
||||
<button
|
||||
block
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2 flex justify-center rounded-r-md bg-primary-500 px-3 py-2 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@click="isShow = true"
|
||||
>
|
||||
发送验证码
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
v-model="identityForm.username"
|
||||
placeholder="请输入用户名"
|
||||
class="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
v-model="identityForm.password"
|
||||
placeholder="请输入密码"
|
||||
class="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
v-model="identityForm.confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
class="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="globalConfig.isAutoOpenAgreement === '1'"
|
||||
class="flex items-center justify-between my-3"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
v-model="agreedToUserAgreement"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
/>
|
||||
<p class="ml-1 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
已阅读并同意
|
||||
<a
|
||||
href="#"
|
||||
class="font-semibold leading-6 text-primary-600 hover:text-primary-500 dark:text-primary-500 dark:hover:text-primary-600"
|
||||
@click="handleClick"
|
||||
>《{{ globalConfig.agreementTitle }}》</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-4">
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="phone"
|
||||
type="text"
|
||||
v-model="identityForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
class="block w-full rounded-md border-0 py-2 px-3 text-gray-900 shadow-sm dark:text-gray-300 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-300 sm:text-sm sm:leading-6 dark:bg-gray-800 dark:focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div>
|
||||
<button
|
||||
@click="handlerSubmit()"
|
||||
type="submit"
|
||||
class="flex w-full my-5 justify-center rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-primary-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
提交认证
|
||||
</button>
|
||||
</div>
|
||||
<SliderCaptcha
|
||||
:show="isShow"
|
||||
@success="handleSendCaptcha()"
|
||||
@close="isShow = false"
|
||||
class="bg-red-500"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
1164
chat/src/components/Settings/AccountManagement.vue
Normal file
97
chat/src/components/Settings/DataManagement.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
import { useChatStore } from '@/store'
|
||||
import { dialog } from '@/utils/dialog'
|
||||
import { Delete } from '@icon-park/vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const loading = ref(false)
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
/* 删除全部非置顶聊天 */
|
||||
async function handleClearConversations() {
|
||||
const dialogInstance = dialog()
|
||||
dialogInstance.warning({
|
||||
title: t('chat.clearConversation'),
|
||||
content: t('chat.clearAllNonFavoriteConversations'),
|
||||
positiveText: t('common.confirm'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await chatStore.delAllGroup()
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-y-auto custom-scrollbar p-1" :class="{ 'max-h-[70vh]': !isMobile }">
|
||||
<!-- 清空对话卡片 -->
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-4 flex flex-col space-y-4"
|
||||
>
|
||||
<!-- 卡片标题 -->
|
||||
<div
|
||||
class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
清空对话记录
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
清空所有非收藏的对话记录。此操作不可撤销,请谨慎操作。
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleClearConversations"
|
||||
class="flex items-center px-4 py-2 rounded-lg bg-red-500 hover:bg-red-600 text-white font-medium text-sm"
|
||||
:disabled="loading"
|
||||
>
|
||||
<Delete theme="outline" size="16" class="mr-2" />
|
||||
<span>清空记录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
/* 暗黑模式下滚动条样式 */
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: rgba(100, 100, 100, 0.5) transparent;
|
||||
}
|
||||
</style>
|
||||
782
chat/src/components/Settings/MemberCenter.vue
Normal file
@@ -0,0 +1,782 @@
|
||||
<script setup lang="ts">
|
||||
import { fetchGetPackageAPI, fetchUseCramiAPI } from '@/api/crami'
|
||||
import { fetchOrderBuyAPI } from '@/api/order'
|
||||
import { fetchSignInAPI, fetchSignLogAPI } from '@/api/signin'
|
||||
import { fetchGetJsapiTicketAPI } from '@/api/user'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
import { message } from '@/utils/message'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { ResData } from '@/api/types'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import MemberPayment from './MemberPayment.vue'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
declare let WeixinJSBridge: any
|
||||
declare let wx: any
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
const loading = ref(true)
|
||||
const packageList = ref<Pkg[]>([])
|
||||
const ms = message()
|
||||
const dialogLoading = ref(false)
|
||||
const model3Name = computed(() => authStore.globalConfig.model3Name || t('goods.basicModelQuota'))
|
||||
const { isMobile } = useBasicLayout()
|
||||
const model4Name = computed(
|
||||
() => authStore.globalConfig.model4Name || t('goods.advancedModelQuota')
|
||||
)
|
||||
const drawMjName = computed(() => authStore.globalConfig.drawMjName || t('goods.drawingQuota'))
|
||||
const isHideModel3Point = computed(() => Number(authStore.globalConfig.isHideModel3Point) === 1)
|
||||
const isHideModel4Point = computed(() => Number(authStore.globalConfig.isHideModel4Point) === 1)
|
||||
const isHideDrawMjPoint = computed(() => Number(authStore.globalConfig.isHideDrawMjPoint) === 1)
|
||||
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,
|
||||
payLtzfStatus,
|
||||
payDuluPayStatus,
|
||||
} = 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'
|
||||
|
||||
if (Number(payLtzfStatus) === 1) return 'ltzf'
|
||||
|
||||
if (Number(payDuluPayStatus) === 1) return 'dulu'
|
||||
|
||||
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 ['hupi']
|
||||
|
||||
if (payPlatform.value === 'dulu') return ['dulu']
|
||||
|
||||
if (payPlatform.value === 'ltzf') 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
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
// 组件挂载时检查登录状态
|
||||
if (checkLoginStatus()) {
|
||||
openDrawerAfter()
|
||||
if (isWxEnv.value) jsapiInitConfig()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 二级页面控制
|
||||
const activeView = ref('main') // 'main'或'payment'
|
||||
const selectedPackage = ref<Pkg | null>(null)
|
||||
|
||||
// 切换到支付页面
|
||||
function showPaymentView(pkg: Pkg) {
|
||||
selectedPackage.value = pkg
|
||||
useGlobalStore.updateOrderInfo({ pkgInfo: pkg })
|
||||
activeView.value = 'payment'
|
||||
}
|
||||
|
||||
// 返回主视图
|
||||
function backToMainView() {
|
||||
activeView.value = 'main'
|
||||
selectedPackage.value = null
|
||||
}
|
||||
|
||||
// 处理支付成功
|
||||
function handlePaymentSuccess() {
|
||||
ms.success(t('goods.purchaseSuccess'))
|
||||
activeView.value = 'main'
|
||||
selectedPackage.value = null
|
||||
|
||||
// 刷新用户信息
|
||||
authStore.getUserInfo()
|
||||
|
||||
// 关闭设置对话框
|
||||
setTimeout(() => {
|
||||
useGlobalStore.updateSettingsDialog(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
packageList.value = []
|
||||
loading.value = true
|
||||
|
||||
// 确保返回主视图,清理资源
|
||||
activeView.value = 'main'
|
||||
selectedPackage.value = null
|
||||
})
|
||||
|
||||
/* 微信环境jsapi注册 */
|
||||
async function jsapiInitConfig() {
|
||||
const url = window.location.href.replace(/#.*$/, '')
|
||||
const res = (await fetchGetJsapiTicketAPI({ url })) as ResData
|
||||
const { appId, nonceStr, timestamp, signature } = res.data
|
||||
if (!appId) return
|
||||
|
||||
wx.config({
|
||||
debug: false,
|
||||
appId,
|
||||
timestamp,
|
||||
nonceStr,
|
||||
signature,
|
||||
jsApiList: ['chooseWXPay'],
|
||||
})
|
||||
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,
|
||||
timeStamp,
|
||||
nonceStr,
|
||||
package: pkg,
|
||||
signType,
|
||||
paySign,
|
||||
},
|
||||
(res: any) => {
|
||||
if (res.err_msg === 'get_brand_wxpay_request:ok') {
|
||||
ms.success(t('goods.purchaseSuccess'))
|
||||
setTimeout(() => {
|
||||
authStore.getUserInfo()
|
||||
}, 500)
|
||||
} else {
|
||||
ms.success(t('goods.paymentNotSuccessful'))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function handleBuyGoods(pkg: Pkg) {
|
||||
if (dialogLoading.value) return
|
||||
|
||||
// 判断是否是微信移动端环境
|
||||
function isWxMobileEnv() {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
// 微信环境
|
||||
const isWxEnv = ua.indexOf('micromessenger') !== -1
|
||||
// 非PC端
|
||||
const isMobile = ua.indexOf('windows') === -1 && ua.indexOf('macintosh') === -1
|
||||
return isWxEnv && isMobile
|
||||
}
|
||||
|
||||
// 如果是微信环境判断有没有开启微信支付,开启了则直接调用jsapi支付即可
|
||||
if (
|
||||
isWxMobileEnv() &&
|
||||
payPlatform.value === 'wechat' &&
|
||||
Number(authStore.globalConfig.payWechatStatus) === 1
|
||||
) {
|
||||
if (typeof WeixinJSBridge == 'undefined') {
|
||||
// 使用事件监听器而不是直接传递回调函数
|
||||
const bridgeReadyHandler = () => {
|
||||
// 在回调中使用onBridgeReady函数处理支付
|
||||
const handlePayment = async () => {
|
||||
const res: ResData = await fetchOrderBuyAPI({
|
||||
goodsId: pkg.id,
|
||||
payType: 'jsapi',
|
||||
})
|
||||
const { success, data } = res
|
||||
if (success) onBridgeReady(data)
|
||||
}
|
||||
handlePayment()
|
||||
}
|
||||
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('WeixinJSBridgeReady', bridgeReadyHandler as EventListener, false)
|
||||
}
|
||||
} else {
|
||||
const res: ResData = await fetchOrderBuyAPI({
|
||||
goodsId: pkg.id,
|
||||
payType: 'jsapi',
|
||||
})
|
||||
const { success, data } = res
|
||||
success && onBridgeReady(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/* 其他场景打开支付窗口 */
|
||||
useGlobalStore.updateOrderInfo({ pkgInfo: pkg })
|
||||
}
|
||||
|
||||
async function openDrawerAfter() {
|
||||
// 首先检查登录状态
|
||||
if (!checkLoginStatus()) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 清空当前套餐列表,避免显示旧数据
|
||||
packageList.value = []
|
||||
// 获取用户最新余额信息
|
||||
await authStore.getUserInfo()
|
||||
// 获取套餐列表
|
||||
const res: ResData = await fetchGetPackageAPI({ status: 1, size: 30 })
|
||||
packageList.value = res.data.rows
|
||||
// 获取签到记录
|
||||
await getSigninLog()
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
loading.value = false
|
||||
console.error('加载会员中心数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const selectName = ref('')
|
||||
const handleSelect = (item: { name: string }) => {
|
||||
selectName.value = item.name
|
||||
cramiSelect.value = false
|
||||
}
|
||||
|
||||
function handleSuccess(pkg: Pkg) {
|
||||
// 检查支付渠道是否启用
|
||||
if (!payChannel.value.length) {
|
||||
ms.warning(t('goods.paymentNotEnabled'))
|
||||
return
|
||||
}
|
||||
|
||||
// 微信移动端环境需要特殊处理
|
||||
if (
|
||||
isWxEnv.value &&
|
||||
payPlatform.value === 'wechat' &&
|
||||
Number(authStore.globalConfig.payWechatStatus) === 1
|
||||
) {
|
||||
// 直接处理JSAPI支付
|
||||
handleBuyGoods(pkg)
|
||||
return
|
||||
}
|
||||
|
||||
// 其他情况切换到支付视图
|
||||
showPaymentView(pkg)
|
||||
}
|
||||
|
||||
function splitDescription(description: string) {
|
||||
return description.split('\n')
|
||||
}
|
||||
|
||||
const code = ref('')
|
||||
const cramiSelect = ref(false)
|
||||
async function useCrami() {
|
||||
if (!code.value.trim()) {
|
||||
ms.info(t('usercenter.pleaseEnterCardDetails'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
await fetchUseCramiAPI({ code: code.value })
|
||||
ms.success(t('usercenter.cardRedeemSuccess'))
|
||||
authStore.getUserInfo()
|
||||
loading.value = false
|
||||
// 清空卡密输入框
|
||||
code.value = ''
|
||||
} catch (error: any) {
|
||||
loading.value = false
|
||||
// 清空卡密输入框
|
||||
code.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 由于globalConfig可能没有showCrami属性,这里默认为true显示卡密兑换
|
||||
const showCrami = ref(true)
|
||||
|
||||
const router = useRouter()
|
||||
const showGoodsDialog = ref(false)
|
||||
|
||||
const openGoodsDialog = () => {
|
||||
showGoodsDialog.value = true
|
||||
}
|
||||
|
||||
// 签到相关状态和方法
|
||||
const signInData = ref<{ signInDate: string; isSigned: boolean }[]>([])
|
||||
const signInLoading = ref(false)
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
|
||||
const days = computed(() => {
|
||||
return signInData.value.map(item => ({
|
||||
...item,
|
||||
day: item.signInDate.split('-').pop()?.replace(/^0/, ''),
|
||||
isToday: item.signInDate === today,
|
||||
}))
|
||||
})
|
||||
|
||||
const consecutiveDays = computed(() => authStore.userInfo.consecutiveDays || 0)
|
||||
const signInModel3Count = computed(() => Number(authStore.globalConfig?.signInModel3Count) || 0)
|
||||
const signInModel4Count = computed(() => Number(authStore.globalConfig?.signInModel4Count) || 0)
|
||||
const signInMjDrawToken = computed(() => Number(authStore.globalConfig?.signInMjDrawToken) || 0)
|
||||
|
||||
const hasSignedInToday = computed(() => {
|
||||
return signInData.value.some(item => item.signInDate === today && item.isSigned)
|
||||
})
|
||||
|
||||
async function getSigninLog() {
|
||||
try {
|
||||
const res: ResData = await fetchSignLogAPI()
|
||||
if (res.success) {
|
||||
signInData.value = res.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载签到数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSignIn() {
|
||||
try {
|
||||
signInLoading.value = true
|
||||
const res: ResData = await fetchSignInAPI()
|
||||
if (res.success) {
|
||||
ms.success('签到成功!')
|
||||
await getSigninLog()
|
||||
authStore.getUserInfo()
|
||||
}
|
||||
signInLoading.value = false
|
||||
} catch (error) {
|
||||
signInLoading.value = false
|
||||
console.error('签到失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function getFirstDayOfMonth(year: number, month: number) {
|
||||
return new Date(year, month, 1).getDay()
|
||||
}
|
||||
|
||||
// 获取用户信息和余额
|
||||
const userBalance = computed(() => authStore.userBalance)
|
||||
const isMember = computed(() => userBalance.value.isMember || false)
|
||||
|
||||
// 登录状态检测
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
|
||||
// 登录检测函数
|
||||
function checkLoginStatus() {
|
||||
console.log('会员中心 - 检查登录状态:', isLogin.value)
|
||||
if (!isLogin.value) {
|
||||
console.log('用户未登录,关闭设置弹窗并打开登录弹窗')
|
||||
// 显示消息提醒
|
||||
ms.warning('请先登录后使用会员中心')
|
||||
// 关闭设置弹窗
|
||||
useGlobalStore.updateSettingsDialog(false)
|
||||
// 打开登录弹窗
|
||||
authStore.setLoginDialog(true)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 监听登录状态变化
|
||||
watch(isLogin, newLoginStatus => {
|
||||
console.log('会员中心 - 登录状态变化:', newLoginStatus)
|
||||
// 如果组件可见但用户登出了,立即关闭设置弹窗并打开登录弹窗
|
||||
if (props.visible && !newLoginStatus) {
|
||||
console.log('用户已登出,关闭设置弹窗并打开登录弹窗')
|
||||
// 显示消息提醒
|
||||
ms.warning('账户已登出,请重新登录后查看')
|
||||
useGlobalStore.updateSettingsDialog(false)
|
||||
authStore.setLoginDialog(true)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加对visible属性的监听,确保组件可见时重新加载数据
|
||||
watch(
|
||||
() => props.visible,
|
||||
isVisible => {
|
||||
if (isVisible) {
|
||||
// 组件显示时立即检查登录状态
|
||||
if (checkLoginStatus()) {
|
||||
openDrawerAfter()
|
||||
if (isWxEnv.value) jsapiInitConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-y-auto custom-scrollbar p-1" :class="{ 'max-h-[70vh]': !isMobile }">
|
||||
<!-- 主视图 -->
|
||||
<div v-if="activeView === 'main'">
|
||||
<!-- 套餐列表卡片 -->
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-4 flex flex-col space-y-4"
|
||||
>
|
||||
<!-- 卡片标题 -->
|
||||
<div
|
||||
class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
套餐列表
|
||||
</div>
|
||||
|
||||
<!-- 套餐列表 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="(item, index) in packageList"
|
||||
:key="index"
|
||||
:class="[
|
||||
item.name == selectName
|
||||
? 'ring-2 ring-primary-500 shadow-md'
|
||||
: 'ring-1 ring-gray-200 dark:ring-gray-700',
|
||||
'rounded-lg p-6 hover:shadow-md bg-white dark:bg-gray-750',
|
||||
]"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<div class="relative">
|
||||
<b class="text-lg font-semibold leading-8 dark:text-white">{{ item.name }}</b>
|
||||
</div>
|
||||
|
||||
<div v-if="!isHideModel3Point" class="flex justify-between items-end mt-4">
|
||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{
|
||||
model3Name
|
||||
}}</span>
|
||||
<span class="font-bold dark:text-white">
|
||||
{{ item.model3Count > 99999 ? '无限额度' : item.model3Count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isHideModel4Point" class="flex justify-between items-end mt-2">
|
||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{
|
||||
model4Name
|
||||
}}</span>
|
||||
<span class="font-bold dark:text-white">
|
||||
{{ item.model4Count > 99999 ? '无限额度' : item.model4Count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isHideDrawMjPoint" class="flex justify-between items-end mt-2">
|
||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{
|
||||
drawMjName
|
||||
}}</span>
|
||||
<span class="font-bold dark:text-white">
|
||||
{{ item.drawMjCount > 99999 ? '无限额度' : item.drawMjCount }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-baseline gap-x-1">
|
||||
<span class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">{{
|
||||
`¥${item.price}`
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<button @click.stop="handleSuccess(item)" class="btn btn-primary btn-md w-full">
|
||||
购买套餐
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-if="item.des"
|
||||
class="mt-4 space-y-2 text-sm leading-6 text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<li
|
||||
v-for="(line, index) in splitDescription(item.des)"
|
||||
:key="index"
|
||||
class="flex gap-x-2"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5 flex-none text-primary-600 dark:text-primary-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{{ line }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 签到和余额并排显示区域 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<!-- 签到日历卡片 - 左侧 -->
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 flex flex-col space-y-4 h-full"
|
||||
>
|
||||
<!-- 卡片标题 -->
|
||||
<div
|
||||
class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
签到奖励
|
||||
</div>
|
||||
|
||||
<!-- 签到信息 -->
|
||||
<div
|
||||
class="bg-gray-50 mb-4 p-3 rounded-lg border border-gray-200 dark:border-gray-700 dark:bg-gray-700"
|
||||
>
|
||||
<span class="dark:text-gray-300">签到赠送:</span>
|
||||
<span v-if="signInModel3Count > 0 && !isHideModel3Point"
|
||||
><b class="mx-2 text-primary-500">{{ signInModel3Count }}</b
|
||||
><span class="dark:text-gray-300">{{ model3Name }}</span></span
|
||||
>
|
||||
<span v-if="signInModel4Count > 0 && !isHideModel4Point"
|
||||
><b class="mx-2 text-primary-500">{{ signInModel4Count }}</b
|
||||
><span class="dark:text-gray-300">{{ model4Name }}</span></span
|
||||
>
|
||||
<span v-if="signInMjDrawToken > 0 && !isHideDrawMjPoint"
|
||||
><b class="mx-2 text-primary-500">{{ signInMjDrawToken }}</b
|
||||
><span class="dark:text-gray-300">{{ drawMjName }}</span></span
|
||||
>
|
||||
<span class="dark:text-gray-300"
|
||||
>(已连续签到<b class="text-red-500 mx-1">{{ consecutiveDays }}</b
|
||||
>天)</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 签到日历 -->
|
||||
<div class="flex-grow">
|
||||
<div
|
||||
class="grid grid-cols-7 text-center text-xs leading-6 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<div>日</div>
|
||||
<div>一</div>
|
||||
<div>二</div>
|
||||
<div>三</div>
|
||||
<div>四</div>
|
||||
<div>五</div>
|
||||
<div>六</div>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-7 text-sm">
|
||||
<div
|
||||
v-for="n in getFirstDayOfMonth(new Date().getFullYear(), new Date().getMonth())"
|
||||
:key="'empty-' + n"
|
||||
class="py-2"
|
||||
></div>
|
||||
<div v-for="day in days" :key="day.signInDate" class="py-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
day.isToday
|
||||
? 'bg-primary-600 text-white'
|
||||
: day.isSigned
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-gray-900 dark:text-gray-100',
|
||||
'hover:bg-gray-200 dark:hover:bg-gray-700 mx-auto flex h-8 w-8 items-center justify-center rounded-full',
|
||||
]"
|
||||
>
|
||||
<time :datetime="day.signInDate">{{ day.day }}</time>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 签到按钮 -->
|
||||
<div class="mt-4 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
@click="handleSignIn"
|
||||
:disabled="hasSignedInToday || signInLoading"
|
||||
class="btn btn-primary btn-md w-full"
|
||||
>
|
||||
<span v-if="signInLoading">签到中...</span>
|
||||
<span v-else-if="hasSignedInToday">已签到</span>
|
||||
<span v-else>签到</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 钱包余额卡片 - 右侧 -->
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 flex flex-col space-y-4 h-full"
|
||||
>
|
||||
<!-- 卡片标题 -->
|
||||
<div
|
||||
class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
额度信息
|
||||
</div>
|
||||
|
||||
<!-- 余额信息 -->
|
||||
<div class="space-y-3">
|
||||
<!-- 基础模型积分 -->
|
||||
<div
|
||||
v-if="!isHideModel3Point"
|
||||
class="flex items-center p-2 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
>
|
||||
<div class="text-gray-500 dark:text-gray-400 w-28">{{ model3Name }}</div>
|
||||
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
{{
|
||||
userBalance.sumModel3Count > 999999
|
||||
? '无限额度'
|
||||
: (userBalance.sumModel3Count ?? 0)
|
||||
}}
|
||||
<span
|
||||
v-if="userBalance.sumModel3Count <= 999999"
|
||||
class="text-sm text-gray-500 dark:text-gray-400 ml-1"
|
||||
>{{ t('usercenter.points') }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级模型积分 -->
|
||||
<div
|
||||
v-if="!isHideModel4Point"
|
||||
class="flex items-center p-2 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
>
|
||||
<div class="text-gray-500 dark:text-gray-400 w-28">{{ model4Name }}</div>
|
||||
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
{{
|
||||
userBalance.sumModel4Count > 99999
|
||||
? '无限额度'
|
||||
: (userBalance.sumModel4Count ?? 0)
|
||||
}}
|
||||
<span
|
||||
v-if="userBalance.sumModel4Count <= 99999"
|
||||
class="text-sm text-gray-500 dark:text-gray-400 ml-1"
|
||||
>{{ t('usercenter.points') }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绘画积分 -->
|
||||
<div
|
||||
v-if="!isHideDrawMjPoint"
|
||||
class="flex items-center p-2 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
>
|
||||
<div class="text-gray-500 dark:text-gray-400 w-28">{{ drawMjName }}</div>
|
||||
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
{{
|
||||
userBalance.sumDrawMjCount > 99999
|
||||
? '无限额度'
|
||||
: (userBalance.sumDrawMjCount ?? 0)
|
||||
}}
|
||||
<span
|
||||
v-if="userBalance.sumDrawMjCount <= 99999"
|
||||
class="text-sm text-gray-500 dark:text-gray-400 ml-1"
|
||||
>{{ t('usercenter.points') }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会员到期时间 -->
|
||||
<div
|
||||
class="flex items-center p-2 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
>
|
||||
<div class="text-gray-500 dark:text-gray-400 w-28">会员状态</div>
|
||||
<div
|
||||
class="text-lg font-bold"
|
||||
:class="isMember ? 'text-red-500' : 'text-gray-500 dark:text-gray-400'"
|
||||
>
|
||||
{{ userBalance.expirationTime ? `${userBalance.expirationTime} 到期` : '非会员' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡密兑换部分移至此处 -->
|
||||
<div
|
||||
v-if="showCrami"
|
||||
class="flex-grow mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div class="text-base font-medium text-gray-900 dark:text-gray-100 mb-3">卡密兑换</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
v-model="code"
|
||||
:placeholder="t('usercenter.enterCardDetails')"
|
||||
class="input input-md w-full"
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
:disabled="loading || !code"
|
||||
@click="useCrami"
|
||||
class="btn btn-primary btn-md w-24"
|
||||
>
|
||||
兑换
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付视图 -->
|
||||
<MemberPayment
|
||||
v-else-if="activeView === 'payment'"
|
||||
:visible="activeView === 'payment'"
|
||||
@back-to-main="backToMainView"
|
||||
@payment-success="handlePaymentSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
/* 暗黑模式下滚动条样式 */
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: rgba(100, 100, 100, 0.5) transparent;
|
||||
}
|
||||
</style>
|
||||
482
chat/src/components/Settings/MemberPayment.vue
Normal file
@@ -0,0 +1,482 @@
|
||||
<script setup lang="ts">
|
||||
import { fetchOrderBuyAPI, fetchOrderQueryAPI } from '@/api/order'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
import { useAuthStore, useGlobalStore } from '@/store'
|
||||
import { message } from '@/utils/message'
|
||||
import { ArrowLeft } from '@icon-park/vue-next'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { ResData } from '@/api/types'
|
||||
import alipay from '@/assets/alipay.png'
|
||||
import wxpay from '@/assets/wxpay.png'
|
||||
import QRCode from '@/components/common/QRCode/index.vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits(['back-to-main', 'payment-success'])
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
const authStore = useAuthStore()
|
||||
const useGlobal = useGlobalStore()
|
||||
const POLL_INTERVAL = 1000
|
||||
const ms = message()
|
||||
const active = ref(true)
|
||||
const payType = ref('wxpay')
|
||||
|
||||
/* 是否是微信环境 */
|
||||
/* 是否是微信移动端环境 */
|
||||
const isWxEnv = computed(() => {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
|
||||
// 判断是否为微信环境
|
||||
const isWxBrowser =
|
||||
ua.match(/MicroMessenger/i) && ua?.match(/MicroMessenger/i)?.[0] === 'micromessenger'
|
||||
|
||||
// 判断是否为非PC端(即移动端)
|
||||
const isMobile = !ua.includes('windows') && !ua.includes('macintosh')
|
||||
|
||||
// 返回是否是微信的移动端环境
|
||||
return isWxBrowser && isMobile
|
||||
})
|
||||
|
||||
/* 开启的支付平台 */
|
||||
const payPlatform = computed(() => {
|
||||
const {
|
||||
payHupiStatus,
|
||||
payEpayStatus,
|
||||
payMpayStatus,
|
||||
payWechatStatus,
|
||||
payLtzfStatus,
|
||||
payDuluPayStatus,
|
||||
} = 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'
|
||||
|
||||
if (Number(payLtzfStatus) === 1) return 'ltzf'
|
||||
|
||||
if (Number(payDuluPayStatus) === 1) return 'dulu'
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
/* 支付平台开启的支付渠道 */
|
||||
const payChannel = computed(() => {
|
||||
const { payEpayChannel, payMpayChannel, payDuluPayChannel } = 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']
|
||||
|
||||
if (payPlatform.value === 'ltzf') return ['wxpay']
|
||||
|
||||
if (payPlatform.value === 'dulu') return payDuluPayChannel ? JSON.parse(payDuluPayChannel) : []
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const plat = computed(() => {
|
||||
return payType.value === 'wxpay' ? t('pay.wechat') : t('pay.alipay')
|
||||
})
|
||||
const countdownRef = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const remainingTime = ref(60)
|
||||
const isCountingDown = ref(false)
|
||||
|
||||
const isRedirectPay = computed(() => {
|
||||
const { payEpayApiPayUrl, payDuluPayRedirect } = authStore.globalConfig
|
||||
return (
|
||||
(payPlatform.value === 'epay' && payEpayApiPayUrl.includes('submit')) ||
|
||||
payPlatform.value === 'mpay' ||
|
||||
(payPlatform.value === 'dulu' && payDuluPayRedirect === '1')
|
||||
)
|
||||
})
|
||||
|
||||
// 倒计时函数
|
||||
function startCountdown() {
|
||||
remainingTime.value = 300 // 5分钟倒计时
|
||||
if (!countdownRef.value) {
|
||||
countdownRef.value = setInterval(() => {
|
||||
remainingTime.value--
|
||||
if (remainingTime.value <= 0) {
|
||||
handleFinish()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 倒计时结束处理
|
||||
function handleFinish() {
|
||||
if (countdownRef.value) {
|
||||
clearInterval(countdownRef.value)
|
||||
countdownRef.value = null
|
||||
}
|
||||
active.value = false
|
||||
ms.warning(t('pay.paymentTimeExpired'))
|
||||
backToMainView()
|
||||
}
|
||||
|
||||
watch(payType, () => {
|
||||
getQrCode()
|
||||
// 重新开始倒计时
|
||||
if (countdownRef.value) {
|
||||
clearInterval(countdownRef.value)
|
||||
countdownRef.value = null
|
||||
}
|
||||
startCountdown()
|
||||
})
|
||||
|
||||
const orderId = ref('')
|
||||
let timer: any
|
||||
const payTypes = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('pay.wechatPay'),
|
||||
value: 'wxpay',
|
||||
icon: wxpay,
|
||||
payChannel: 'wxpay',
|
||||
},
|
||||
{
|
||||
label: t('pay.alipayPay'),
|
||||
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) {
|
||||
stopPolling()
|
||||
ms.success(t('pay.paymentSuccess'))
|
||||
active.value = false
|
||||
authStore.getUserInfo()
|
||||
|
||||
// 支付成功后通知父组件
|
||||
emit('payment-success')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const orderInfo = computed(() => useGlobal?.orderInfo)
|
||||
const url_qrcode = ref('')
|
||||
const qrCodeloading = ref(true)
|
||||
const redirectloading = ref(true)
|
||||
const redirectUrl = ref('')
|
||||
|
||||
// 返回主视图
|
||||
function backToMainView() {
|
||||
cleanupResources()
|
||||
emit('back-to-main')
|
||||
}
|
||||
|
||||
/* 请求二维码 */
|
||||
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 } = res
|
||||
if (!success) {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
backToMainView()
|
||||
qrCodeloading.value = false
|
||||
redirectloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* 跳转支付 */
|
||||
function handleRedPay() {
|
||||
window.open(redirectUrl.value)
|
||||
}
|
||||
|
||||
// 清理所有资源
|
||||
function cleanupResources() {
|
||||
// 停止轮询
|
||||
stopPolling()
|
||||
|
||||
// 清理倒计时
|
||||
if (countdownRef.value) {
|
||||
clearInterval(countdownRef.value)
|
||||
countdownRef.value = null
|
||||
}
|
||||
|
||||
// 清理其他资源
|
||||
url_qrcode.value = ''
|
||||
orderId.value = ''
|
||||
active.value = false
|
||||
}
|
||||
|
||||
async function handleOpenPayment() {
|
||||
await getQrCode()
|
||||
if (!timer) {
|
||||
// 检查定时器是否已存在
|
||||
timer = setInterval(() => {
|
||||
queryOrderStatus()
|
||||
}, POLL_INTERVAL)
|
||||
}
|
||||
|
||||
// 启动倒计时
|
||||
startCountdown()
|
||||
}
|
||||
|
||||
// 清除定时器的函数
|
||||
function stopPolling() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null // 清除定时器后将变量设置为 null
|
||||
}
|
||||
}
|
||||
|
||||
// 监听visible变化,处理资源
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal && !oldVal) {
|
||||
// 变为可见时
|
||||
active.value = true
|
||||
handleOpenPayment()
|
||||
} else if (!newVal && oldVal) {
|
||||
// 变为不可见时
|
||||
cleanupResources()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
handleOpenPayment()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupResources()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-y-auto custom-scrollbar p-2" :class="{ 'max-h-[70vh]': !isMobile }">
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-4"
|
||||
>
|
||||
<!-- 卡片标题 -->
|
||||
<div class="flex items-center mb-4 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<button @click="backToMainView" class="btn-icon btn-md mr-2">
|
||||
<ArrowLeft size="18" />
|
||||
</button>
|
||||
<div class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ t('pay.productPayment') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
<div>
|
||||
<span class="whitespace-nowrap font-bold">{{ t('pay.amountDue') }}</span>
|
||||
<span class="ml-1 text-xl font-bold tracking-tight">{{
|
||||
`¥${orderInfo.pkgInfo?.price}`
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex">
|
||||
<span class="whitespace-nowrap font-bold">{{ t('pay.packageName') }}</span
|
||||
><span class="ml-2"> {{ orderInfo.pkgInfo?.name }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex justify-center"
|
||||
:class="[isMobile ? 'flex-col' : 'flex-row', isRedirectPay ? 'flex-row-reverse' : '']"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center justify-center my-3 relative">
|
||||
<!-- 微信登录风格的加载动画 -->
|
||||
<div
|
||||
v-if="qrCodeloading && !isRedirectPay"
|
||||
class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<div
|
||||
class="animate-spin rounded-full h-10 w-10 border-b-2 border-primary-600 dark:border-primary-400"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="qrCodeloading"
|
||||
class="w-[240px] h-[240px] rounded-lg bg-gray-200 dark:bg-gray-700 animate-pulse"
|
||||
></div>
|
||||
|
||||
<!-- epay -->
|
||||
<QRCode
|
||||
v-if="
|
||||
payPlatform === 'epay' && !qrCodeloading && !redirectloading && !isRedirectPay
|
||||
"
|
||||
:value="url_qrcode"
|
||||
:size="240"
|
||||
/>
|
||||
<QRCode
|
||||
v-if="
|
||||
payPlatform === 'dulu' && !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">{{ t('pay.siteAdminEnabledRedirect') }}</span>
|
||||
|
||||
<!-- mapy 跳转支付 -->
|
||||
<button
|
||||
v-if="isRedirectPay"
|
||||
type="button"
|
||||
class="btn btn-primary btn-md"
|
||||
:disabled="redirectloading"
|
||||
@click="handleRedPay"
|
||||
>
|
||||
{{ t('pay.clickToPay') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- dulu -->
|
||||
<!-- <iframe
|
||||
v-if="payPlatform === 'dulu' && !redirectloading"
|
||||
class="w-[280px] h-[280px] scale-90"
|
||||
:src="url_qrcode"
|
||||
frameborder="0"
|
||||
/> -->
|
||||
|
||||
<!-- hupi -->
|
||||
<iframe
|
||||
v-if="payPlatform === 'hupi' && !redirectloading"
|
||||
class="w-[280px] h-[280px] scale-90"
|
||||
:src="url_qrcode"
|
||||
frameborder="0"
|
||||
/>
|
||||
|
||||
<!-- ltzf -->
|
||||
<img
|
||||
v-if="payPlatform === 'ltzf' && !redirectloading"
|
||||
:src="url_qrcode"
|
||||
class="w-[280px] h-[280px] scale-90"
|
||||
alt="QRCode"
|
||||
/>
|
||||
</div>
|
||||
<span v-if="!isRedirectPay" class="flex items-center justify-center text-lg">
|
||||
{{ t('pay.open') }} {{ plat }} {{ t('pay.scanToPay') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col" :class="[isMobile ? 'w-full ' : ' ml-10 w-[200] ']">
|
||||
<div
|
||||
class="flex items-center justify-center mt-6 w-full font-bold text-sm"
|
||||
:class="[isMobile ? 'mb-2' : 'mb-10']"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
<span>{{ t('pay.completePaymentWithin') }}</span>
|
||||
<span class="inline-block w-16 text-primary-500 text-center">
|
||||
{{ remainingTime }}秒
|
||||
</span>
|
||||
<span>{{ t('pay.timeToCompletePayment') }}</span>
|
||||
</div>
|
||||
<!-- 支付方式选择区域 -->
|
||||
<div class="mt-6 space-y-6">
|
||||
<div v-for="pay in payTypes" :key="pay.value" class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
:id="pay.value"
|
||||
name="payment-method"
|
||||
:value="pay.value"
|
||||
v-model="payType"
|
||||
class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
/>
|
||||
<label
|
||||
:for="pay.value"
|
||||
class="ml-3 block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300"
|
||||
>
|
||||
<img class="h-4 object-contain mr-2 inline-block" :src="pay.icon" alt="" />
|
||||
{{ pay.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
/* 暗黑模式下滚动条样式 */
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: rgba(100, 100, 100, 0.5) transparent;
|
||||
}
|
||||
</style>
|
||||
100
chat/src/components/Settings/NoticeDialog.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAppStore, useAuthStore } from '@/store'
|
||||
import { MdPreview } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/preview.css'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const appStore = useAppStore()
|
||||
const darkMode = computed(() => appStore.theme === 'dark')
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const { noticeInfo } = authStore.globalConfig
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
|
||||
function openDrawerAfter() {
|
||||
// 刷新全局配置数据,确保获取最新的公告信息
|
||||
authStore.getGlobalConfig().catch(error => {
|
||||
console.error('获取最新公告信息失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
isVisible => {
|
||||
if (isVisible) {
|
||||
// 当组件变为可见时刷新数据
|
||||
openDrawerAfter()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
openDrawerAfter()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-y-auto custom-scrollbar p-1" :class="{ 'max-h-[70vh]': !isMobile }">
|
||||
<!-- 公告信息卡片 -->
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-4 flex flex-col space-y-4"
|
||||
>
|
||||
<!-- 卡片标题 -->
|
||||
<div
|
||||
class="text-base font-semibold text-gray-900 dark:text-gray-100 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
{{ globalConfig.noticeTitle || '平台公告' }}
|
||||
</div>
|
||||
|
||||
<!-- 公告内容 -->
|
||||
<div class="overflow-y-auto" :class="{ 'max-h-[calc(70vh-120px)]': !isMobile }">
|
||||
<MdPreview
|
||||
editorId="preview-only"
|
||||
:modelValue="noticeInfo"
|
||||
:theme="darkMode ? 'dark' : 'light'"
|
||||
class="dark:bg-gray-700 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
/* 暗黑模式下滚动条样式 */
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: rgba(100, 100, 100, 0.5) transparent;
|
||||
}
|
||||
</style>
|
||||
78
chat/src/components/Settings/UserAgreement.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAppStore, useAuthStore } from '@/store'
|
||||
import { MdPreview } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/preview.css'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const appStore = useAppStore()
|
||||
const darkMode = computed(() => appStore.theme === 'dark')
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
const { isMobile } = useBasicLayout()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-y-auto custom-scrollbar p-1" :class="{ 'max-h-[70vh]': !isMobile }">
|
||||
<!-- 用户协议卡片 -->
|
||||
<div
|
||||
class="p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-4 flex flex-col space-y-4"
|
||||
>
|
||||
<!-- 卡片标题 -->
|
||||
<div
|
||||
class="text-base font-semibold text-gray-900 dark:text-gray-100 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
{{ globalConfig.agreementTitle || '用户协议' }}
|
||||
</div>
|
||||
|
||||
<!-- 用户协议内容 -->
|
||||
<div
|
||||
class="overflow-y-auto custom-scrollbar"
|
||||
:class="{ 'max-h-[calc(70vh-160px)]': !isMobile }"
|
||||
>
|
||||
<MdPreview
|
||||
editorId="preview-only"
|
||||
:modelValue="globalConfig.agreementInfo"
|
||||
:theme="darkMode ? 'dark' : 'light'"
|
||||
class="dark:bg-gray-700 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
/* 暗黑模式下滚动条样式 */
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: rgba(100, 100, 100, 0.5) transparent;
|
||||
}
|
||||
</style>
|
||||
228
chat/src/components/SettingsDialog.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-if="props.visible"
|
||||
class="fixed inset-0 z-[9000] flex items-center justify-center bg-gray-900 bg-opacity-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-gray-750 rounded-lg shadow-lg flex flex-col"
|
||||
:class="
|
||||
isMobile ? 'w-full h-full' : 'h-[80vh] rounded-lg shadow-lg w-full max-w-5xl p-4 mx-2'
|
||||
"
|
||||
>
|
||||
<!-- 标题部分 -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-xl font-bold dark:text-white">设置</span>
|
||||
<button @click="handleClose" class="btn-icon btn-md">
|
||||
<Close size="20" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- 主体部分 -->
|
||||
<div class="flex flex-grow">
|
||||
<!-- 左边标签栏 -->
|
||||
<div class="w-1/5 bg-white dark:bg-gray-750 rounded-lg">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="switchTab(index)"
|
||||
class="relative flex items-center gap-3 px-3 py-3 my-1 break-all rounded-lg cursor-pointer group dark:hover:bg-gray-700 font-medium text-sm"
|
||||
:class="{
|
||||
'bg-gray-50 text-primary-600 dark:bg-gray-700 dark:text-primary-400':
|
||||
activeTab === index,
|
||||
'text-gray-700 dark:text-gray-400': activeTab !== index,
|
||||
}"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
|
||||
<!-- 添加退出登录按钮,位于标签栏最下方 -->
|
||||
<div class="mt-auto">
|
||||
<div
|
||||
@click="showLogoutConfirmation"
|
||||
class="relative flex items-center gap-3 px-3 py-3 my-1 break-all rounded-lg cursor-pointer group font-medium text-sm text-red-500 dark:text-red-400 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
退出登录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右边内容区域 -->
|
||||
<div class="w-4/5 bg-white dark:bg-gray-750 rounded-lg ml-4">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="!isTabSwitching" key="loaded-content">
|
||||
<keep-alive>
|
||||
<component
|
||||
v-if="tabs[activeTab]"
|
||||
:is="tabs[activeTab].component"
|
||||
:key="activeKey"
|
||||
:visible="props.visible && !isTabSwitching"
|
||||
></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
<div v-else key="loading-placeholder" class="flex justify-center items-center h-full">
|
||||
<div class="animate-pulse flex space-x-4">
|
||||
<div class="flex-1 space-y-4 py-1">
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||||
<div class="space-y-2">
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAuthStore, useGlobalStoreWithOut } from '@/store'
|
||||
import { dialog } from '@/utils/dialog'
|
||||
import { Close } from '@icon-park/vue-next' // Only Close icon needed now
|
||||
import { computed, markRaw, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import AccountManagement from './Settings/AccountManagement.vue'
|
||||
import MemberCenter from './Settings/MemberCenter.vue'
|
||||
import NoticeDialog from './Settings/NoticeDialog.vue'
|
||||
import UserAgreement from './Settings/UserAgreement.vue'
|
||||
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
const { isMobile } = useBasicLayout() // Still needed for :class binding
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const globalConfig = computed(() => authStore.globalConfig)
|
||||
|
||||
// Use markRaw to prevent components from becoming reactive
|
||||
const tabs = computed(() => {
|
||||
const baseTabs = [
|
||||
{ name: '账户管理', component: markRaw(AccountManagement) },
|
||||
{ name: '会员中心', component: markRaw(MemberCenter) },
|
||||
// { name: '数据管理', component: markRaw(DataManagement) },
|
||||
{ name: '网站公告', component: markRaw(NoticeDialog) },
|
||||
]
|
||||
|
||||
// 只有当 globalConfig.isAutoOpenAgreement === '1' 时才添加用户协议选项
|
||||
if (globalConfig.value.isAutoOpenAgreement === '1') {
|
||||
baseTabs.push({ name: '用户协议', component: markRaw(UserAgreement) })
|
||||
}
|
||||
|
||||
return baseTabs
|
||||
})
|
||||
|
||||
// Desktop-specific state
|
||||
const activeTab = ref(
|
||||
useGlobalStore.settingsActiveTab >= 0 && useGlobalStore.settingsActiveTab < tabs.value.length
|
||||
? useGlobalStore.settingsActiveTab
|
||||
: 0
|
||||
)
|
||||
const activeKey = ref(Date.now())
|
||||
const isTabSwitching = ref(false)
|
||||
|
||||
// Tab switching function (Desktop only)
|
||||
function switchTab(index: number) {
|
||||
if (index >= 0 && index < tabs.value.length && index !== activeTab.value) {
|
||||
isTabSwitching.value = true
|
||||
activeTab.value = index
|
||||
useGlobalStore.settingsActiveTab = index
|
||||
nextTick(() => {
|
||||
activeKey.value = Date.now()
|
||||
setTimeout(() => {
|
||||
isTabSwitching.value = false
|
||||
}, 50) // Reduced delay
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for global changes (for desktop sync)
|
||||
watch(
|
||||
() => useGlobalStore.settingsActiveTab,
|
||||
newVal => {
|
||||
// Check if the dialog is visible and it's not mobile view
|
||||
if (
|
||||
props.visible &&
|
||||
!isMobile.value &&
|
||||
newVal >= 0 &&
|
||||
newVal < tabs.value.length &&
|
||||
newVal !== activeTab.value
|
||||
) {
|
||||
switchTab(newVal)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Watch for visibility changes (for desktop refresh)
|
||||
watch(
|
||||
() => props.visible,
|
||||
isVisible => {
|
||||
if (isVisible && !isMobile.value) {
|
||||
// Sync with global store on open for desktop
|
||||
const targetTab =
|
||||
useGlobalStore.settingsActiveTab >= 0 &&
|
||||
useGlobalStore.settingsActiveTab < tabs.value.length
|
||||
? useGlobalStore.settingsActiveTab
|
||||
: 0
|
||||
if (activeTab.value !== targetTab) {
|
||||
activeTab.value = targetTab
|
||||
}
|
||||
activeKey.value = Date.now() // Refresh key on open
|
||||
isTabSwitching.value = false // Ensure not stuck loading
|
||||
} else if (!isVisible) {
|
||||
// Optionally reset tab when closed, or keep last state?
|
||||
// activeTab.value = 0;
|
||||
isTabSwitching.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true } // Run immediately to set initial state
|
||||
)
|
||||
|
||||
// Close Handler
|
||||
function handleClose() {
|
||||
useGlobalStore.updateSettingsDialog(false)
|
||||
}
|
||||
|
||||
// Logout Handler
|
||||
function showLogoutConfirmation() {
|
||||
const dialogInstance = dialog()
|
||||
dialogInstance.warning({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
authStore.logOut()
|
||||
handleClose() // Close settings after logout
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// onMounted: Initial state sync handled by immediate watchers
|
||||
onMounted(() => {
|
||||
// console.log('Desktop SettingsDialog mounted');
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
120
chat/src/components/common/DropdownMenu/MenuItem.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
// 是否激活状态
|
||||
active?: boolean
|
||||
// 是否禁用
|
||||
disabled?: boolean
|
||||
// 是否显示分割线
|
||||
divider?: boolean
|
||||
// 图标(支持URL或组件)
|
||||
icon?: string
|
||||
// 菜单项标题
|
||||
title?: string
|
||||
// 菜单项描述
|
||||
description?: string
|
||||
// 自定义样式类
|
||||
className?: string
|
||||
// 是否显示右箭头
|
||||
showArrow?: boolean
|
||||
// 菜单项尺寸
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
active: false,
|
||||
disabled: false,
|
||||
divider: false,
|
||||
icon: '',
|
||||
title: '',
|
||||
description: '',
|
||||
className: '',
|
||||
showArrow: false,
|
||||
size: 'md',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
if (!props.disabled) {
|
||||
emit('click', event)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 分割线 -->
|
||||
<div v-if="divider" class="menu-divider" role="separator" />
|
||||
|
||||
<!-- 菜单项 -->
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
'menu-item',
|
||||
`menu-item-${size}`,
|
||||
{
|
||||
'menu-item-active': active,
|
||||
'menu-item-disabled': disabled,
|
||||
},
|
||||
className,
|
||||
]"
|
||||
@click="handleClick"
|
||||
role="menuitem"
|
||||
:tabindex="disabled ? -1 : 0"
|
||||
:aria-disabled="disabled"
|
||||
>
|
||||
<!-- 图标区域 -->
|
||||
<div v-if="icon || $slots.icon" class="menu-item-icon">
|
||||
<slot name="icon">
|
||||
<img
|
||||
v-if="icon"
|
||||
:src="icon"
|
||||
:alt="`${title}图标`"
|
||||
class="w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="menu-item-content">
|
||||
<slot>
|
||||
<div v-if="title" class="menu-item-title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div v-if="description" class="menu-item-description">
|
||||
{{ description }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div v-if="$slots.suffix || showArrow || active" class="flex-shrink-0">
|
||||
<slot name="suffix">
|
||||
<!-- 激活状态的勾选图标 -->
|
||||
<svg
|
||||
v-if="active"
|
||||
class="w-4 h-4 text-primary-600 dark:text-primary-400"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<!-- 右箭头 -->
|
||||
<svg
|
||||
v-else-if="showArrow"
|
||||
class="w-4 h-4 text-gray-400 dark:text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
5
chat/src/components/common/DropdownMenu/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import DropdownMenu from './index.vue'
|
||||
import MenuItem from './MenuItem.vue'
|
||||
|
||||
export { DropdownMenu, MenuItem }
|
||||
export default DropdownMenu
|
||||
280
chat/src/components/common/DropdownMenu/index.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
// 定义位置类型,添加auto选项
|
||||
type Position =
|
||||
| 'bottom-left'
|
||||
| 'bottom-right'
|
||||
| 'bottom-center'
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'top-center'
|
||||
| 'auto'
|
||||
|
||||
interface Props {
|
||||
// 菜单是否打开
|
||||
modelValue?: boolean
|
||||
// 触发器内容插槽名称
|
||||
trigger?: string
|
||||
// 菜单位置
|
||||
position?: Position
|
||||
// 最大高度
|
||||
maxHeight?: string
|
||||
// 最小宽度
|
||||
minWidth?: string
|
||||
// 是否禁用
|
||||
disabled?: boolean
|
||||
// 自定义菜单样式类
|
||||
menuClass?: string
|
||||
// 自定义触发器样式类
|
||||
triggerClass?: string
|
||||
// 点击外部是否关闭
|
||||
closeOnClickOutside?: boolean
|
||||
// 按下ESC是否关闭
|
||||
closeOnEscape?: boolean
|
||||
// z-index值
|
||||
zIndex?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
position: 'bottom-left',
|
||||
maxHeight: '60vh',
|
||||
minWidth: '200px',
|
||||
disabled: false,
|
||||
menuClass: '',
|
||||
triggerClass: '',
|
||||
closeOnClickOutside: true,
|
||||
closeOnEscape: true,
|
||||
zIndex: 50,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
open: []
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const menuRef = ref<HTMLElement>()
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
|
||||
// 自动检测位置的响应式变量
|
||||
const autoPosition = ref<Exclude<Position, 'auto'>>('bottom-left')
|
||||
|
||||
// 检测位置函数
|
||||
const detectPosition = () => {
|
||||
if (props.position !== 'auto' || !triggerRef.value) return
|
||||
|
||||
const triggerRect = triggerRef.value.getBoundingClientRect()
|
||||
const viewportHeight = window.innerHeight
|
||||
|
||||
// 计算触发器相对于视口的垂直位置
|
||||
const triggerCenterY = triggerRect.top + triggerRect.height / 2
|
||||
|
||||
// 只检测垂直方向:如果触发器在屏幕上半部分,向下展开;否则向上展开
|
||||
const isTopHalf = triggerCenterY < viewportHeight / 2
|
||||
const verticalDirection = isTopHalf ? 'bottom' : 'top'
|
||||
|
||||
// 水平方向固定使用right对齐
|
||||
const horizontalDirection = 'right'
|
||||
|
||||
// 组合最终位置
|
||||
const newPosition = `${verticalDirection}-${horizontalDirection}` as Exclude<Position, 'auto'>
|
||||
autoPosition.value = newPosition
|
||||
|
||||
// 添加调试信息(开发时可用)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Auto position detection:', {
|
||||
triggerRect,
|
||||
viewportHeight,
|
||||
triggerCenterY,
|
||||
isTopHalf,
|
||||
verticalDirection,
|
||||
horizontalDirection,
|
||||
finalPosition: newPosition,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 计算最终位置
|
||||
const finalPosition = computed(() => {
|
||||
if (props.position === 'auto') {
|
||||
return autoPosition.value
|
||||
}
|
||||
return props.position || 'bottom-left'
|
||||
})
|
||||
|
||||
// 计算位置样式类,使用finalPosition而不是props.position
|
||||
const positionClasses = computed(() => {
|
||||
const position = finalPosition.value
|
||||
const classes = {
|
||||
'bottom-left': 'menu-items-bottom',
|
||||
'bottom-right': 'menu-items-bottom menu-items-right-aligned',
|
||||
'bottom-center': 'menu-items-bottom menu-items-center',
|
||||
'top-left': 'menu-items-top',
|
||||
'top-right': 'menu-items-top menu-items-right-aligned',
|
||||
'top-center': 'menu-items-top menu-items-center',
|
||||
}
|
||||
return classes[position] || classes['bottom-left']
|
||||
})
|
||||
|
||||
// 菜单样式类 - 使用全局CSS类
|
||||
const menuClasses = computed(() => {
|
||||
const baseClasses = ['menu-items', 'custom-scrollbar', positionClasses.value, props.menuClass]
|
||||
return baseClasses.filter(Boolean).join(' ')
|
||||
})
|
||||
|
||||
// 切换菜单状态
|
||||
const toggleMenu = () => {
|
||||
if (props.disabled) return
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
// 打开菜单
|
||||
function open() {
|
||||
if (props.disabled) return
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
// 关闭菜单
|
||||
function close() {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
// 处理点击外部关闭菜单
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (!props.closeOnClickOutside || !isOpen.value) return
|
||||
|
||||
const target = event.target as Node
|
||||
const menuElement = menuRef.value
|
||||
const triggerElement = triggerRef.value
|
||||
|
||||
if (
|
||||
menuElement &&
|
||||
triggerElement &&
|
||||
!menuElement.contains(target) &&
|
||||
!triggerElement.contains(target)
|
||||
) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理ESC键关闭
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (props.closeOnEscape && event.key === 'Escape' && isOpen.value) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听菜单打开状态变化
|
||||
watch(isOpen, (newValue, oldValue) => {
|
||||
if (newValue && !oldValue) {
|
||||
// 菜单从关闭变为打开时,重新检测位置
|
||||
if (props.position === 'auto') {
|
||||
// 使用nextTick确保DOM更新完成
|
||||
nextTick(() => {
|
||||
detectPosition()
|
||||
})
|
||||
}
|
||||
emit('open')
|
||||
} else if (!newValue && oldValue) {
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
if (props.closeOnClickOutside) {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}
|
||||
if (props.closeOnEscape) {
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
toggle: toggleMenu,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu relative">
|
||||
<!-- 触发器 -->
|
||||
<div
|
||||
ref="triggerRef"
|
||||
:class="['cursor-pointer', triggerClass, { 'opacity-50 cursor-not-allowed': disabled }]"
|
||||
@click="toggleMenu"
|
||||
role="button"
|
||||
:aria-expanded="isOpen"
|
||||
:aria-haspopup="true"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<slot name="trigger" :isOpen="isOpen" :disabled="disabled">
|
||||
<button
|
||||
type="button"
|
||||
class="menu-button flex items-center px-3 py-2 text-sm font-medium rounded-lg bg-transparent hover:bg-gray-50 dark:hover:bg-gray-750 text-gray-600 dark:text-gray-400"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span>点击展开菜单</span>
|
||||
<svg
|
||||
class="ml-2 w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isOpen }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- 菜单内容 -->
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-show="isOpen"
|
||||
ref="menuRef"
|
||||
:class="menuClasses"
|
||||
:style="{
|
||||
maxHeight: maxHeight,
|
||||
minWidth: minWidth,
|
||||
zIndex: zIndex,
|
||||
overflowY: 'auto',
|
||||
}"
|
||||
role="menu"
|
||||
:aria-hidden="!isOpen"
|
||||
>
|
||||
<slot name="menu" :close="close" :isOpen="isOpen">
|
||||
<div>
|
||||
<div class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300">请添加菜单内容</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
19
chat/src/components/common/ImageViewer/GlobalImageViewer.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<ImageViewer
|
||||
v-model:visible="isVisible"
|
||||
:image-url="currentImageUrl"
|
||||
:file-name="currentFileName"
|
||||
@close="handleClose"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ImageViewer from './index.vue'
|
||||
import { useImageViewer } from './useImageViewer'
|
||||
|
||||
const { isVisible, currentImageUrl, currentFileName, closeImageViewer } = useImageViewer()
|
||||
|
||||
function handleClose() {
|
||||
closeImageViewer()
|
||||
}
|
||||
</script>
|
||||
490
chat/src/components/common/ImageViewer/index.vue
Normal file
@@ -0,0 +1,490 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-90 backdrop-blur-sm"
|
||||
@click="handleBackgroundClick"
|
||||
@wheel.prevent="handleWheel"
|
||||
>
|
||||
<!-- 工具栏 -->
|
||||
<div
|
||||
class="absolute top-4 left-1/2 transform -translate-x-1/2 z-10 flex items-center space-x-2 bg-black bg-opacity-50 rounded-lg px-4 py-2"
|
||||
>
|
||||
<!-- 缩小 -->
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
@click="zoomOut"
|
||||
:disabled="scale <= minScale"
|
||||
title="缩小 (Ctrl + -)"
|
||||
>
|
||||
<Minus size="20" />
|
||||
</button>
|
||||
|
||||
<!-- 缩放比例显示 -->
|
||||
<span class="text-white text-sm min-w-[60px] text-center">
|
||||
{{ Math.round(scale * 100) }}%
|
||||
</span>
|
||||
|
||||
<!-- 放大 -->
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
@click="zoomIn"
|
||||
:disabled="scale >= maxScale"
|
||||
title="放大 (Ctrl + +)"
|
||||
>
|
||||
<Plus size="20" />
|
||||
</button>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="w-px h-6 bg-gray-400"></div>
|
||||
|
||||
<!-- 逆时针旋转 -->
|
||||
<button class="toolbar-btn" @click="rotateLeft" title="逆时针旋转 (Ctrl + ←)">
|
||||
<span class="text-lg">↺</span>
|
||||
</button>
|
||||
|
||||
<!-- 顺时针旋转 -->
|
||||
<button class="toolbar-btn" @click="rotateRight" title="顺时针旋转 (Ctrl + →)">
|
||||
<span class="text-lg">↻</span>
|
||||
</button>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="w-px h-6 bg-gray-400"></div>
|
||||
|
||||
<!-- 重置 -->
|
||||
<button class="toolbar-btn" @click="reset" title="重置 (Ctrl + 0)">
|
||||
<Refresh size="20" />
|
||||
</button>
|
||||
|
||||
<!-- 保存 -->
|
||||
<button class="toolbar-btn" @click="save" title="保存 (Ctrl + S)">
|
||||
<Download size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
class="absolute top-4 right-4 z-10 p-2 rounded-full bg-black bg-opacity-50 text-white hover:bg-opacity-70 transition-all duration-200"
|
||||
@click="close"
|
||||
title="关闭 (ESC)"
|
||||
>
|
||||
<Close size="24" />
|
||||
</button>
|
||||
|
||||
<!-- 图片容器 -->
|
||||
<div
|
||||
ref="imageContainer"
|
||||
class="relative w-full h-full flex items-center justify-center overflow-hidden cursor-grab"
|
||||
:class="{ 'cursor-grabbing': isDragging }"
|
||||
@mousedown="startDrag"
|
||||
@mousemove="drag"
|
||||
@mouseup="stopDrag"
|
||||
@mouseleave="stopDrag"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<img
|
||||
ref="imageRef"
|
||||
:src="imageUrl"
|
||||
class="max-w-none max-h-none transition-transform duration-300 ease-out select-none"
|
||||
:style="imageStyle"
|
||||
alt="预览图片"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="text-white text-lg">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="text-white text-lg">图片加载失败</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<div
|
||||
class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-white text-sm bg-black bg-opacity-50 px-3 py-1 rounded-full"
|
||||
>
|
||||
拖拽移动 • 滚轮缩放 • ESC 关闭
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, Download, Minus, Plus, Refresh } from '@icon-park/vue-next'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
imageUrl: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'close'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
fileName: 'image',
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 图片状态
|
||||
const imageRef = ref<HTMLImageElement>()
|
||||
const imageContainer = ref<HTMLDivElement>()
|
||||
const loading = ref(true)
|
||||
const error = ref(false)
|
||||
|
||||
// 变换状态
|
||||
const scale = ref(1)
|
||||
const rotation = ref(0)
|
||||
const translateX = ref(0)
|
||||
const translateY = ref(0)
|
||||
|
||||
// 缩放限制
|
||||
const minScale = 0.1
|
||||
const maxScale = 5
|
||||
|
||||
// 拖拽状态
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ x: 0, y: 0 })
|
||||
const dragOffset = ref({ x: 0, y: 0 })
|
||||
|
||||
// 图片原始尺寸
|
||||
const originalSize = ref({ width: 0, height: 0 })
|
||||
|
||||
// 计算图片样式
|
||||
const imageStyle = computed(() => ({
|
||||
transform: `translate(${translateX.value}px, ${translateY.value}px) scale(${scale.value}) rotate(${rotation.value}deg)`,
|
||||
transformOrigin: 'center center',
|
||||
}))
|
||||
|
||||
// 处理图片加载
|
||||
function handleImageLoad() {
|
||||
loading.value = false
|
||||
error.value = false
|
||||
|
||||
if (imageRef.value) {
|
||||
originalSize.value = {
|
||||
width: imageRef.value.naturalWidth,
|
||||
height: imageRef.value.naturalHeight,
|
||||
}
|
||||
|
||||
// 自动适配屏幕尺寸
|
||||
autoFit()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片加载错误
|
||||
function handleImageError() {
|
||||
loading.value = false
|
||||
error.value = true
|
||||
}
|
||||
|
||||
// 自动适配屏幕尺寸
|
||||
function autoFit() {
|
||||
if (!imageRef.value || !imageContainer.value) return
|
||||
|
||||
const containerRect = imageContainer.value.getBoundingClientRect()
|
||||
const imageWidth = originalSize.value.width
|
||||
const imageHeight = originalSize.value.height
|
||||
|
||||
// 计算适合的缩放比例
|
||||
const scaleX = (containerRect.width * 0.9) / imageWidth
|
||||
const scaleY = (containerRect.height * 0.9) / imageHeight
|
||||
const fitScale = Math.min(scaleX, scaleY, 1) // 不超过原始尺寸
|
||||
|
||||
scale.value = fitScale
|
||||
}
|
||||
|
||||
// 放大
|
||||
function zoomIn() {
|
||||
const newScale = Math.min(scale.value * 1.2, maxScale)
|
||||
scale.value = newScale
|
||||
}
|
||||
|
||||
// 缩小
|
||||
function zoomOut() {
|
||||
const newScale = Math.max(scale.value / 1.2, minScale)
|
||||
scale.value = newScale
|
||||
}
|
||||
|
||||
// 顺时针旋转
|
||||
function rotateRight() {
|
||||
rotation.value = (rotation.value + 90) % 360
|
||||
}
|
||||
|
||||
// 逆时针旋转
|
||||
function rotateLeft() {
|
||||
rotation.value = (rotation.value - 90 + 360) % 360
|
||||
}
|
||||
|
||||
// 重置
|
||||
function reset() {
|
||||
scale.value = 1
|
||||
rotation.value = 0
|
||||
translateX.value = 0
|
||||
translateY.value = 0
|
||||
autoFit()
|
||||
}
|
||||
|
||||
// 保存图片
|
||||
async function save() {
|
||||
if (!props.imageUrl) return
|
||||
|
||||
try {
|
||||
console.log('全局预览器开始下载图片:', props.imageUrl)
|
||||
|
||||
// 尝试直接下载(适用于同域或支持CORS的图片)
|
||||
try {
|
||||
const response = await fetch(props.imageUrl, {
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
})
|
||||
console.log('全局预览器fetch响应状态:', response.status, response.statusText)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
console.log('全局预览器blob大小:', blob.size, 'blob类型:', blob.type)
|
||||
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${props.fileName}.${getFileExtension(props.imageUrl)}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
|
||||
window.URL.revokeObjectURL(url)
|
||||
console.log('全局预览器图片下载完成')
|
||||
return
|
||||
} catch (fetchError) {
|
||||
console.log('全局预览器fetch下载失败,尝试canvas方法:', fetchError)
|
||||
|
||||
// 如果fetch失败,尝试使用canvas方法(适用于跨域图片)
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
canvas.width = img.naturalWidth
|
||||
canvas.height = img.naturalHeight
|
||||
|
||||
ctx?.drawImage(img, 0, 0)
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
if (blob) {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${props.fileName}.${getFileExtension(props.imageUrl)}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
console.log('全局预览器canvas下载完成')
|
||||
resolve(true)
|
||||
} else {
|
||||
reject(new Error('无法生成图片blob'))
|
||||
}
|
||||
}, 'image/png')
|
||||
} catch (canvasError) {
|
||||
reject(canvasError)
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('图片加载失败'))
|
||||
}
|
||||
|
||||
img.src = props.imageUrl
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存图片失败:', error)
|
||||
|
||||
// 最后的备用方案:直接打开图片链接
|
||||
try {
|
||||
const a = document.createElement('a')
|
||||
a.href = props.imageUrl
|
||||
a.download = `${props.fileName}.${getFileExtension(props.imageUrl)}`
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
console.log('全局预览器已在新窗口打开图片')
|
||||
} catch (linkError) {
|
||||
console.error('全局预览器所有下载方法都失败了:', linkError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
function getFileExtension(url: string): string {
|
||||
const match = url.match(/\.([^.]+)$/)
|
||||
return match ? match[1] : 'png'
|
||||
}
|
||||
|
||||
// 处理鼠标滚轮缩放
|
||||
function handleWheel(event: WheelEvent) {
|
||||
event.preventDefault()
|
||||
|
||||
const delta = event.deltaY > 0 ? -1 : 1
|
||||
const zoomFactor = 1.1
|
||||
const newScale =
|
||||
delta > 0
|
||||
? Math.min(scale.value * zoomFactor, maxScale)
|
||||
: Math.max(scale.value / zoomFactor, minScale)
|
||||
|
||||
scale.value = newScale
|
||||
}
|
||||
|
||||
// 开始拖拽
|
||||
function startDrag(event: MouseEvent) {
|
||||
if (event.button !== 0) return // 只响应左键
|
||||
|
||||
isDragging.value = true
|
||||
dragStart.value = { x: event.clientX, y: event.clientY }
|
||||
dragOffset.value = { x: translateX.value, y: translateY.value }
|
||||
}
|
||||
|
||||
// 拖拽中
|
||||
function drag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const deltaX = event.clientX - dragStart.value.x
|
||||
const deltaY = event.clientY - dragStart.value.y
|
||||
|
||||
translateX.value = dragOffset.value.x + deltaX
|
||||
translateY.value = dragOffset.value.y + deltaY
|
||||
}
|
||||
|
||||
// 停止拖拽
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// 处理背景点击
|
||||
function handleBackgroundClick(event: MouseEvent) {
|
||||
if (event.target === event.currentTarget) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭预览
|
||||
function close() {
|
||||
emit('update:visible', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (!props.visible) return
|
||||
|
||||
const { key, ctrlKey, metaKey } = event
|
||||
const isCtrl = ctrlKey || metaKey
|
||||
|
||||
switch (key) {
|
||||
case 'Escape':
|
||||
close()
|
||||
break
|
||||
case '=':
|
||||
case '+':
|
||||
if (isCtrl) {
|
||||
event.preventDefault()
|
||||
zoomIn()
|
||||
}
|
||||
break
|
||||
case '-':
|
||||
if (isCtrl) {
|
||||
event.preventDefault()
|
||||
zoomOut()
|
||||
}
|
||||
break
|
||||
case '0':
|
||||
if (isCtrl) {
|
||||
event.preventDefault()
|
||||
reset()
|
||||
}
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
if (isCtrl) {
|
||||
event.preventDefault()
|
||||
rotateLeft()
|
||||
}
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (isCtrl) {
|
||||
event.preventDefault()
|
||||
rotateRight()
|
||||
}
|
||||
break
|
||||
case 's':
|
||||
if (isCtrl) {
|
||||
event.preventDefault()
|
||||
save()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 监听visible变化,重置状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
newVisible => {
|
||||
if (newVisible) {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
reset()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听imageUrl变化
|
||||
watch(
|
||||
() => props.imageUrl,
|
||||
() => {
|
||||
if (props.visible) {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
reset()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar-btn {
|
||||
@apply p-2 rounded-md text-white hover:bg-white hover:bg-opacity-20 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.toolbar-btn:not(:disabled):hover {
|
||||
@apply bg-white bg-opacity-20;
|
||||
}
|
||||
|
||||
.toolbar-btn:not(:disabled):active {
|
||||
@apply bg-white bg-opacity-30;
|
||||
}
|
||||
</style>
|
||||
55
chat/src/components/common/ImageViewer/useImageViewer.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { App, ref } from 'vue'
|
||||
import ImageViewer from './index.vue'
|
||||
|
||||
// 全局状态
|
||||
const isVisible = ref(false)
|
||||
const currentImageUrl = ref('')
|
||||
const currentFileName = ref('image')
|
||||
|
||||
// 图片预览器实例
|
||||
export interface ImageViewerOptions {
|
||||
imageUrl: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
// 打开图片预览器
|
||||
export function openImageViewer(options: ImageViewerOptions) {
|
||||
currentImageUrl.value = options.imageUrl
|
||||
currentFileName.value = options.fileName || 'image'
|
||||
isVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭图片预览器
|
||||
export function closeImageViewer() {
|
||||
isVisible.value = false
|
||||
currentImageUrl.value = ''
|
||||
currentFileName.value = 'image'
|
||||
}
|
||||
|
||||
// 图片预览器状态
|
||||
export function useImageViewer() {
|
||||
return {
|
||||
isVisible,
|
||||
currentImageUrl,
|
||||
currentFileName,
|
||||
openImageViewer,
|
||||
closeImageViewer,
|
||||
}
|
||||
}
|
||||
|
||||
// 全局安装插件
|
||||
export default {
|
||||
install(app: App) {
|
||||
// 注册全局组件
|
||||
app.component('ImageViewer', ImageViewer)
|
||||
|
||||
// 注册全局方法
|
||||
app.config.globalProperties.$imageViewer = {
|
||||
open: openImageViewer,
|
||||
close: closeImageViewer,
|
||||
}
|
||||
|
||||
// 提供全局状态
|
||||
app.provide('imageViewer', useImageViewer())
|
||||
},
|
||||
}
|
||||
77
chat/src/components/common/QRCode/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<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>
|
||||
21
chat/src/components/common/SvgIcon/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
interface Props {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
||||
class: (attrs.class as string) || '',
|
||||
style: (attrs.style as string) || 'width: 1em, height: 1em',
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon :icon="icon" v-bind="bindAttrs" />
|
||||
</template>
|
||||
53
chat/src/components/common/Watermark/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/store'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
const watermark = computed(() => {
|
||||
// 已登录状态
|
||||
const userId = authStore.userInfo.id
|
||||
const nickname = authStore.userInfo.nickname
|
||||
|
||||
// 未登录状态
|
||||
if (!isLogin.value) {
|
||||
return `游客(${userId})`
|
||||
}
|
||||
// 如果有用户名,格式:username # 用户ID
|
||||
if (nickname) {
|
||||
return `${nickname}(${userId})`
|
||||
}
|
||||
|
||||
// 如果没有用户名,格式:# 用户ID
|
||||
return `(${userId})`
|
||||
})
|
||||
|
||||
function createWatermark() {
|
||||
if (!watermark.value) return ''
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return ''
|
||||
|
||||
canvas.width = 240
|
||||
canvas.height = 180
|
||||
|
||||
ctx.rotate((-20 * Math.PI) / 180)
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'
|
||||
ctx.font = '16px Arial'
|
||||
ctx.fillText(watermark.value, -20, 100)
|
||||
|
||||
return `url(${canvas.toDataURL()})`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="watermark"
|
||||
class="fixed inset-0 pointer-events-none select-none z-50"
|
||||
:style="{
|
||||
backgroundImage: createWatermark(),
|
||||
backgroundRepeat: 'repeat',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
10
chat/src/hooks/useBasicLayout.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
export function useBasicLayout() {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMobile = breakpoints.smaller('sm')
|
||||
const isSmallMd = breakpoints.smaller('md')
|
||||
const isSmallLg = breakpoints.smaller('lg')
|
||||
const isSmallXl = breakpoints.smaller('xl')
|
||||
|
||||
return { isMobile, isSmallMd, isSmallLg, isSmallXl }
|
||||
}
|
||||
41
chat/src/hooks/useLanguage.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import { computed } from 'vue'
|
||||
|
||||
// 定义本地化配置
|
||||
const locales = {
|
||||
'en-US': {
|
||||
locale: 'en-US',
|
||||
name: 'English',
|
||||
dateFormat: 'MM/DD/YYYY',
|
||||
timeFormat: 'h:mm:ss A',
|
||||
},
|
||||
'zh-CN': {
|
||||
locale: 'zh-CN',
|
||||
name: '简体中文',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
timeFormat: 'HH:mm:ss',
|
||||
},
|
||||
'zh-TW': {
|
||||
locale: 'zh-TW',
|
||||
name: '繁體中文',
|
||||
dateFormat: 'YYYY-MM-DD',
|
||||
timeFormat: 'HH:mm:ss',
|
||||
},
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 计算当前语言配置
|
||||
const currentLocale = computed(() => {
|
||||
const lang = appStore.language
|
||||
return locales[lang] || locales['zh-CN']
|
||||
})
|
||||
|
||||
// 监听 appStore.language 的变化,并据此更新 Vue I18n 的语言环境
|
||||
// watch(() => appStore.language, (newLocale) => {
|
||||
// setLocale(newLocale);
|
||||
// }, { immediate: true });
|
||||
|
||||
return { currentLocale }
|
||||
}
|
||||
26
chat/src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function useTheme() {
|
||||
const html = document.documentElement
|
||||
|
||||
const init = () => {
|
||||
const saved = localStorage.getItem('theme')
|
||||
if (saved && (saved === 'dark' || saved === 'light')) {
|
||||
html.dataset.theme = saved
|
||||
html.classList.toggle('dark', saved === 'dark')
|
||||
} else {
|
||||
// 检测系统主题偏好
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
html.dataset.theme = prefersDark ? 'dark' : 'light'
|
||||
html.classList.toggle('dark', prefersDark)
|
||||
localStorage.setItem('theme', html.dataset.theme)
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
const next = html.dataset.theme === 'dark' ? 'light' : 'dark'
|
||||
html.dataset.theme = next
|
||||
html.classList.toggle('dark', next === 'dark')
|
||||
localStorage.setItem('theme', next)
|
||||
}
|
||||
|
||||
return { init, toggle }
|
||||
}
|
||||
596
chat/src/locales/en-US.json
Normal file
@@ -0,0 +1,596 @@
|
||||
{
|
||||
"language": "en",
|
||||
"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",
|
||||
"update": "Update",
|
||||
"download": "Download",
|
||||
"noData": "No Data",
|
||||
"wrong": "Something went wrong, please try again later.",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"verify": "Verify",
|
||||
"unauthorizedTips": "Unauthorized, please verify first.",
|
||||
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"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",
|
||||
|
||||
"size": "Size:",
|
||||
"generatedContentDisclaimer": "AI can make mistakes. Consider checking important information. All rights reserved ©",
|
||||
"square1": "Square [1:1]",
|
||||
"illustration": "Illustration [4:3]",
|
||||
"wallpaper": "Wallpaper [16:9]",
|
||||
"media": "Media [3:4]",
|
||||
"poster": "Poster [9:16]",
|
||||
"square": "Square",
|
||||
"landscape": "Landscape",
|
||||
"portrait": "Portrait",
|
||||
"chatDialogue": "Chat Dialogue",
|
||||
"startNewConversationPrompt": "Click the button below to start a new conversation",
|
||||
"newConversation": "New Conversation",
|
||||
"networkModeEnabledContextInvalid": "Network mode enabled, context invalidated!",
|
||||
"networkModeDisabled": "Network mode disabled!",
|
||||
"pointsMall": "Points Mall",
|
||||
"toggleTheme": "Toggle Theme",
|
||||
"signInReward": "Sign-In Reward",
|
||||
"networkMode": "Network Mode",
|
||||
"searchHistoryConversations": "Search History Conversations",
|
||||
"announcement": "Announcement",
|
||||
"clear": "Clear",
|
||||
"remaining": "",
|
||||
"ordinaryPoints": "Ordinary Points",
|
||||
"advancedPoints": "Advanced Points",
|
||||
"drawingPoints": "Drawing Points",
|
||||
"points": "Points",
|
||||
"clearConversation": "Clear Conversation",
|
||||
"clearAllNonFavoriteConversations": "Clear all non-favorite conversations?",
|
||||
"more": "More",
|
||||
"collapse": "Collapse",
|
||||
"myApps": "My Apps",
|
||||
"appSquare": "App Square",
|
||||
"favorites": "Favorites",
|
||||
"todayConversations": "Today",
|
||||
"Conversations": "",
|
||||
"historyConversations": "History",
|
||||
"favoriteConversations": "Favorite",
|
||||
"unfavorite": "Unfavorite",
|
||||
"rename": "Rename",
|
||||
"deleteConversation": "Delete",
|
||||
"me": "You",
|
||||
"onlineSearch": "Online Search",
|
||||
"mindMap": "Mind Map",
|
||||
"fileAnalysis": "File Analysis",
|
||||
"delete": "Delete",
|
||||
"regenerate": "Regenerate",
|
||||
"pause": "Pause",
|
||||
"loading": "Loading...",
|
||||
"readAloud": "Read Aloud",
|
||||
"vipCenter": "VIP Center",
|
||||
"U1": "🔍 U1",
|
||||
"U2": "🔍 U2",
|
||||
"U3": "🔍 U3",
|
||||
"U4": "🔍 U4",
|
||||
"V1": "🪄 V1",
|
||||
"V2": "🪄 V2",
|
||||
"V3": "🪄 V3",
|
||||
"V4": "🪄 V4",
|
||||
"panLeft": "⬅️ Pan Left",
|
||||
"panRight": "➡️ Pan Right",
|
||||
"panUp": "⬆️ Pan Up",
|
||||
"panDown": "⬇️ Pan Down",
|
||||
"zoomIn15x": "↔️ Zoom 1.5x",
|
||||
"zoomIn2x": "↔️ Zoom 2x",
|
||||
"minorTransform": "Vary(Subtle)",
|
||||
"strongTransform": "Vary(Strong)",
|
||||
"enlargeImage": "Enlarge image {order}",
|
||||
"transformImage": "Transform image {order}",
|
||||
"expandDrawing": "Expand drawing",
|
||||
"advancedTransform": "Advanced transform",
|
||||
"translateImage": "Translate image",
|
||||
"enlargeImagePrefix": "Enlarge image ",
|
||||
"enlargeImageSuffix": "",
|
||||
"transformImagePrefix": "Transform image ",
|
||||
"transformImageSuffix": "",
|
||||
"imageToImage": "Image to Image",
|
||||
"faceConsistency": "Face Consistency",
|
||||
"styleConsistency": "Style Consistency",
|
||||
"selectAppOrTopic": "Select an application or topic for quick conversation"
|
||||
},
|
||||
"app": {
|
||||
"sampleTemplate": "Sample Template",
|
||||
"exploreInfinitePossibilities": "Explore infinite possibilities, create a smart future with AI",
|
||||
"searchAppNameQuickFind": "Search app names, quick find applications...",
|
||||
"allCategories": "All Categories",
|
||||
"noModelConfigured": "No specific application model has been configured by the administrator, please contact them to set it up~"
|
||||
},
|
||||
"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",
|
||||
"sign": "Signature"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"draw": {
|
||||
"use": "Use",
|
||||
"download": "Download",
|
||||
"delete": "Delete",
|
||||
"zoom": "Zoom:",
|
||||
"U1": "U1",
|
||||
"U2": "U2",
|
||||
"U3": "U3",
|
||||
"U4": "U4",
|
||||
"regenerateOnce": "Regenerate",
|
||||
"transform": "Transform:",
|
||||
"V1": "V1",
|
||||
"V2": "V2",
|
||||
"V3": "V3",
|
||||
"V4": "V4",
|
||||
"pan": "Pan:",
|
||||
"panLeft": "⬅️",
|
||||
"panRight": "➡️",
|
||||
"panUp": "⬆️",
|
||||
"panDown": "⬇️",
|
||||
"transformZoom": "Zoom Transform",
|
||||
"zoom1_5x": "Zoom 1.5x",
|
||||
"zoom2x": "Zoom 2x",
|
||||
"minorTransform": "Minor Transform",
|
||||
"strongTransform": "Strong Transform",
|
||||
"regionalRedraw": "Regional Redraw",
|
||||
"regionalRedraw1": "Regional Redraw (Select the Area to Change)",
|
||||
"submitTask": "Submit Task",
|
||||
"selectSuiteForZoom": "Action: Select a suite to zoom",
|
||||
"selectSuiteForTransform": "Action: Select a suite for transformation",
|
||||
"regeneratingImage": "Action: Regenerating the image",
|
||||
"drawingInProgress": "Action: Rapid drawing in progress...",
|
||||
"tryDifferentPrompt": "Execute: Try a different prompt!",
|
||||
"statusWaiting": "Waiting",
|
||||
"statusDrawing": "Drawing",
|
||||
"statusSuccess": "Success",
|
||||
"statusFailure": "Failure",
|
||||
"statusTimeout": "Timeout",
|
||||
"downloadImageTitle": "Download Image",
|
||||
"downloadImageContent": "Download the current image",
|
||||
"downloadButtonText": "Download",
|
||||
"cancelButtonText": "Cancel",
|
||||
"deleteRecordTitle": "Delete Record",
|
||||
"deleteRecordContent": "Delete the current drawing record?",
|
||||
"deleteButtonText": "Delete",
|
||||
"submitZoomDrawingSuccess": "Zoom drawing task submitted successfully, please wait for it to finish!",
|
||||
"submitRedrawSuccess": "Redraw task submitted successfully, please wait for it to finish!",
|
||||
"submitTransformDrawingSuccess": "Transform drawing task submitted successfully, please wait for it to finish!",
|
||||
"submitEnlargeDrawingSuccess": "Enlarge drawing task submitted successfully, please wait for it to finish!",
|
||||
"submitAdvancedTransformDrawingSuccess": "Advanced transform drawing task submitted successfully, please wait for it to finish!",
|
||||
"submitRegionalRedrawSuccess": "Regional redraw task submitted successfully, please wait for it to finish!",
|
||||
"drawingRecordDeleted": "Drawing record has been deleted!",
|
||||
"queueing": "Queueing...",
|
||||
"drawing": "Drawing...",
|
||||
"storing": "Storing image...",
|
||||
"drawingFailed": "Drawing Failed",
|
||||
"pointsRefunded": "Points Refunded!",
|
||||
"submitDrawingTaskSuccess": "Drawing task submitted successfully, please wait for it to finish!",
|
||||
"defaultStyle": "Default Style",
|
||||
"expressiveStyle": "Expressive Style",
|
||||
"cuteStyle": "Cute Style",
|
||||
"scenicStyle": "Scenic Style",
|
||||
"standardQuality": "Standard",
|
||||
"generalQuality": "General",
|
||||
"highDefinitionQuality": "High Definition",
|
||||
"ultraHighDefinitionQuality": "Ultra High Definition",
|
||||
"enterDescription": "Please enter descriptive words!",
|
||||
"optimizationFailed": "Optimization failed!",
|
||||
"professionalDrawing": "Professional Drawing",
|
||||
"parameterExplanation": "Parameter Explanation: Generate image size ratio",
|
||||
"imageSize": "Image Size",
|
||||
"modelSelection": "Model Selection",
|
||||
"tooltipMJ": "MJ: General-purpose realistic model",
|
||||
"tooltipNIJI": "NIJI: Anime style, suitable for 2D models",
|
||||
"version": "Version",
|
||||
"style": "Style",
|
||||
"parameters": "Parameters",
|
||||
"parametersTooltip": "Use parameters wisely to achieve more ideal results!",
|
||||
"quality": "Quality",
|
||||
"chaos": "Chaos",
|
||||
"chaosDescription": "Value range: 0-100, --chaos or --c",
|
||||
"chaosExplanation": "Chaos level, can be understood as the space for AI to think outside the box",
|
||||
"chaosAdvice": "The smaller the value, the more reliable, with the default of 0 being the most precise",
|
||||
"stylization": "Stylization",
|
||||
"stylizationDescription": "Stylization: --stylize or --s, range 1-1000",
|
||||
"parameterExplanation1": "Parameter explanation: The higher the number, the richer and more artistic the visual presentation",
|
||||
"setting": "Setting",
|
||||
"carryParameters": "Carry Parameters",
|
||||
"autoCarryParameters": "Whether to automatically carry parameters",
|
||||
"carryOn": "On: Carries the parameters we have configured",
|
||||
"carryOff": "Off: Uses the parameters we customize in the command",
|
||||
"imageToImage": "Image to Image",
|
||||
"clickOrDrag": "Click or drag an image here to use as input",
|
||||
"supportFormats": "Supports PNG and JPG formats",
|
||||
"remainingPoints": "Remaining Points",
|
||||
"refresh": "Refresh",
|
||||
"accountInfo": "Account Information",
|
||||
"points": "Points",
|
||||
"paintingSingleUse": "Painting:",
|
||||
"imageGenerationSingleUse": "Generation:",
|
||||
"enlargementSingleUse": "Enlargement:",
|
||||
"submitDrawingTask": "Enter keywords, submit drawing task",
|
||||
"optimize": "Optimize",
|
||||
"enterDrawingKeywords": "Enter drawing keywords. For example: A colorful cat, cute, cartoon",
|
||||
"unnecessaryElements": "Unnecessary Elements",
|
||||
"exclusionPrompt": "Example: Generate a room image, but exclude the bed, you can fill in 'bed'!",
|
||||
"workingContents": "Working Contents",
|
||||
"currentTasks": "Current tasks in progress",
|
||||
"goToAIDrawingSquare": "Click to go to the AI Drawing Square",
|
||||
"tasksInProgress": "tasks are currently in progress. Please wait patiently for the drawing to complete. You can visit other pages and return later to see the results!",
|
||||
"myDrawings": "My Drawings",
|
||||
"aiDrawingSquare": "AI Drawing Square",
|
||||
"sizeAdjustment": "Size Adjustment",
|
||||
"keywordSearchPlaceholder": "Prompt Keyword Search"
|
||||
},
|
||||
"pay": {
|
||||
"membershipMarket": "Membership Market",
|
||||
"sizeAdjustment": "Size Adjustment",
|
||||
"memberPackage": "Limited Time Member Package",
|
||||
"permanentAddOnCard": "Permanent Add-On Card",
|
||||
"baseModelQuota": "Base Model Quota",
|
||||
"advancedModelQuota": "Advanced Model Quota",
|
||||
"MJDrawingQuota": "MJ Drawing Quota",
|
||||
"packageValidity": "Package Validity",
|
||||
"days": "days",
|
||||
"permanent": "Permanent",
|
||||
"points": "Points",
|
||||
"welcomeTipMobile": "Explore freely, welcome to our online store!",
|
||||
"welcomeTipDesktop": "Explore freely, welcome to our online store, thank you for choosing us, let's start a delightful shopping journey together!",
|
||||
"paymentNotEnabled": "Payment has not been enabled by the admin!",
|
||||
"purchaseSuccess": "Purchase successful, enjoy your product!",
|
||||
"paymentNotComplete": "You have not completed the payment yet!",
|
||||
"wechat": "WeChat",
|
||||
"alipay": "Alipay",
|
||||
"wechatPay": "WeChat Pay",
|
||||
"alipayPay": "Alipay Pay",
|
||||
"paymentSuccess": "Congratulations, your payment was successful. Enjoy your purchase!",
|
||||
"paymentTimeout": "Payment timeout, please place your order again!",
|
||||
"productPayment": "Product Payment",
|
||||
"amountDue": "Amount Due:",
|
||||
"packageName": "Package Name:",
|
||||
"packageDescription": "Package Description:",
|
||||
"siteAdminEnabledRedirect": "The site administrator has enabled redirect payment",
|
||||
"clickToPay": "Click to Proceed to Payment",
|
||||
"completePaymentWithin": "Please complete the payment within",
|
||||
"timeToCompletePayment": "!",
|
||||
"open": "Open",
|
||||
"scanToPay": "Scan to Pay"
|
||||
},
|
||||
"mindmap": {
|
||||
"title": "Mind Map",
|
||||
"yourNeeds": "Your Needs?",
|
||||
"inputPlaceholder": "Please enter a brief description of the content you want to generate, AI will produce a complete markdown content and its mind map for you!",
|
||||
"generateMindMapButton": "Generate Mind Map",
|
||||
"contentRequirements": "Content Requirements",
|
||||
"tryDemoButton": "Try a Demo",
|
||||
"usageCredits": "Base credits per use: 1",
|
||||
"exportHTML": "Export HTML",
|
||||
"exportPNG": "Export PNG",
|
||||
"exportSVG": "Export SVG"
|
||||
},
|
||||
"usercenter": {
|
||||
"defaultSignature": "I am an AI robot based on deep learning and natural language processing technologies, aimed at providing users with efficient, accurate, and personalized intelligent services.",
|
||||
"syncComplete": "Data synchronization completed",
|
||||
"personalCenter": "Personal Center",
|
||||
"logOut": "Log Out",
|
||||
"myUsageRecord": "My Usage Record on This Site",
|
||||
"basicModelCredits": "Basic Model Credits:",
|
||||
"advancedModelCredits": "Advanced Model Credits:",
|
||||
"basicModelUsage": "Basic Model Usage:",
|
||||
"advancedModelUsage": "Advanced Model Usage:",
|
||||
"drawingUsageCredits": "Drawing Usage Credits:",
|
||||
"bindWeChat": "Bind WeChat:",
|
||||
"clickToBindWeChat": "Click to Bind WeChat",
|
||||
"weChatBound": "WeChat Bound",
|
||||
"syncVisitorData": "Click to Sync Visitor Data",
|
||||
"points": "Points",
|
||||
"membershipExpiration": "Membership Expiration Date:",
|
||||
"editInfoDescription": "Edit personal information, view more details",
|
||||
"myDetails": "My Details",
|
||||
"myWallet": "My Wallet",
|
||||
"basicInfo": "Basic Information",
|
||||
"userBasicSettings": "User Basic Settings",
|
||||
"avatarPlaceholder": "Please enter your avatar URL",
|
||||
"usernamePlaceholder": "Edit your username",
|
||||
"signaturePlaceholder": "Edit your signature",
|
||||
|
||||
"passwordManagement": "Password Management",
|
||||
"inviteBenefits": "Invite for Benefits",
|
||||
"clickToLogin": "Log In",
|
||||
"notLoggedIn": "Not Logged In",
|
||||
"avatar": "Avatar",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"inviteeStatus": "Invitee Status",
|
||||
"inviteTime": "Invite Time",
|
||||
"rewardStatus": "Reward Status",
|
||||
"certified": "Certified",
|
||||
"notActivated": "Not Activated",
|
||||
"rewardReceived": "Reward Received",
|
||||
"waitingConfirmation": "Waiting for Confirmation",
|
||||
"linkGeneratedSuccess": "Invitation link generated successfully",
|
||||
"generateLinkFirst": "Please generate your exclusive invitation link first!",
|
||||
"linkCopiedSuccess": "Exclusive invitation link copied successfully!",
|
||||
"copyNotSupported": "Automatic copying is not supported on this device, please copy manually!",
|
||||
"inviteForBenefits": "Invite Users, Earn Benefits!",
|
||||
"myInviteCode": "My Invitation Code",
|
||||
"generateInviteCode": "Generate Exclusive Invite Code",
|
||||
"copyInviteLink": "Copy Exclusive Invite Link",
|
||||
"inviteOneUser": "Inviting a user grants",
|
||||
"basicModelCredits1": "basic model credits+",
|
||||
"advancedModelCredits1": "advanced model credits+",
|
||||
"mjDrawingCredits": "MJ drawing credits",
|
||||
"receiveInvitation": "Invited users receive",
|
||||
"creditsEnd": "credits",
|
||||
"invitationRecord": "Invitation Record",
|
||||
"passwordMinLength": "The minimum password length is 6 characters",
|
||||
"passwordMaxLength": "The maximum password length is 30 characters",
|
||||
"enterPassword": "Please enter a password",
|
||||
"reenterPassword": "Please re-enter your password",
|
||||
"passwordsNotMatch": "The passwords do not match",
|
||||
"passwordUpdateSuccess": "Password updated successfully, please log in again!",
|
||||
"changeYourPassword": "Change Your Password",
|
||||
"oldPassword": "Old Password",
|
||||
"newPassword": "New Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"reloginAfterPasswordChange": "You will need to log in again after updating your password!",
|
||||
"updateYourPassword": "Update Your Password",
|
||||
"passwordRequirements": "Password Requirements",
|
||||
"newPasswordInstructions": "To create a new password, you must meet all the following requirements:",
|
||||
"minimumCharacters": "At least 6 characters",
|
||||
"maximumCharacters": "No more than 30 characters",
|
||||
"requireNumber": "Must contain at least one number",
|
||||
"orderNumber": "Order Number",
|
||||
"rechargeType": "Recharge Type",
|
||||
"basicModelQuota": "Basic Model Quota",
|
||||
"advancedModelQuota": "Advanced Model Quota",
|
||||
"mjDrawingQuota": "MJ Drawing Quota",
|
||||
"validity": "Validity",
|
||||
"rechargeTime": "Recharge Time",
|
||||
"enterCardSecret": "Please enter the card secret first!",
|
||||
"cardRedeemSuccess": "Card redeemed successfully, enjoy your use!",
|
||||
"userWalletBalance": "User Wallet Balance",
|
||||
"basicModelBalance": "Basic Model Balance",
|
||||
"creditUsageNote": "Each conversation consumes different credits depending on the model!",
|
||||
"advancedModelBalance": "Advanced Model Balance",
|
||||
"modelConsumptionNote": "Each conversation consumes different credits depending on the model!",
|
||||
"mjDrawingBalance": "MJ Drawing Balance",
|
||||
"drawingConsumptionNote": "Different credits are consumed based on drawing actions!",
|
||||
"cardRecharge": "Card Recharge",
|
||||
"enterCardDetails": "Please paste or enter your card details!",
|
||||
"pleaseEnterCardDetails": "Please enter card details",
|
||||
"exchange": "Exchange",
|
||||
"buyCardSecret": "Buy Card Secret",
|
||||
"rechargeRecords": "Recharge Records",
|
||||
"packagePurchase": "Package Purchase",
|
||||
"buyPackage": "Buy Package"
|
||||
},
|
||||
"siderBar": {
|
||||
"signInReward": "Sign-in Reward",
|
||||
"themeSwitch": "Theme Switch",
|
||||
"personalCenter": "Personal Center",
|
||||
"loginAccount": "Log In Account"
|
||||
},
|
||||
"notice": {
|
||||
"doNotRemind24h": "Do not remind again for 24 hours"
|
||||
},
|
||||
"login": {
|
||||
"enterUsername": "Please enter your username",
|
||||
"usernameLength": "Username must be between 2 and 30 characters",
|
||||
"enterPassword": "Please enter your password",
|
||||
"passwordLength": "Password must be between 6 and 30 characters",
|
||||
"enterEmail": "Please enter your email address",
|
||||
"emailValid": "Please enter a valid email address",
|
||||
"enterCaptcha": "Please enter the captcha",
|
||||
"emailPhone": "Email / Phone",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"registrationSuccess": "Account registration successful, start your experience!",
|
||||
"loginSuccess": "Account login successful, start your experience!",
|
||||
"registerTitle": "Register",
|
||||
|
||||
"enterContact": "Please provide your ",
|
||||
|
||||
"enterCode": "Please enter the verification code",
|
||||
"sendVerificationCode": "Send Verification Code",
|
||||
"optionalInvitationCode": "Invitation Code [Optional]",
|
||||
"registerAccount": "Register Account",
|
||||
"alreadyHaveAccount": "Already have an account?",
|
||||
"goToLogin": "Go to Login",
|
||||
"password": "Password",
|
||||
"enterYourPassword": "Please enter your password",
|
||||
"rememberAccount": "Remember account",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"loginAccount": "Log In Account",
|
||||
"noAccount": "Don't have an account?",
|
||||
"register": "Register",
|
||||
"orUse": "or use",
|
||||
"scanLogin": "Scan to Log In",
|
||||
"wechatLogin": "WeChat Login",
|
||||
"wechatScanFailed": "Failed WeChat QR code login? Use",
|
||||
"useWechatScan": "Use WeChat to Scan and Log In"
|
||||
},
|
||||
"share": {
|
||||
"orderAmount": "Order Amount",
|
||||
"productType": "Product Type",
|
||||
"status": "Status",
|
||||
"commissionRate": "Commission Rate",
|
||||
"commission": "Commission",
|
||||
"orderTime": "Order Time",
|
||||
"purchasePackage": "Purchase Package",
|
||||
"accounted": "Accounted",
|
||||
"generateInviteCodeSuccess": "Invitation code generated successfully",
|
||||
"withdrawalTime": "Withdrawal Time",
|
||||
"withdrawalAmount": "Withdrawal Amount",
|
||||
"withdrawalChannel": "Withdrawal Channel",
|
||||
"withdrawalStatus": "Withdrawal Status",
|
||||
"withdrawalRemarks": "Withdrawal Remarks",
|
||||
"auditor": "Auditor",
|
||||
"alipay": "Alipay",
|
||||
"wechat": "WeChat",
|
||||
"paid": "Paid",
|
||||
"rejected": "Rejected",
|
||||
"inReview": "In Review",
|
||||
"avatar": "Avatar",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"inviteeStatus": "Invitee Status",
|
||||
"registered": "Registered",
|
||||
"pendingActivation": "Pending Activation",
|
||||
"registrationTime": "Registration Time",
|
||||
"lastLogin": "Last Login",
|
||||
"requestInviteCodeFirst": "Please request your invitation code first",
|
||||
"linkCopiedSuccess": "Share link copied successfully",
|
||||
"title": "Referral Program",
|
||||
"description": "Join us and share in success! Welcome to our distribution page, become our partner and create a bright future together!",
|
||||
"defaultSalesOutletName": "Rookie Referral Officer",
|
||||
"myReferrals": "My Referrals",
|
||||
"currencyUnit": "Yuan",
|
||||
"remainingAmount": "Remaining Withdrawable Amount",
|
||||
"withdrawingAmount": "Amount in Withdrawal",
|
||||
"withdrawNow": "Withdraw Now",
|
||||
"minimumWithdrawalPrefix": "Minimum",
|
||||
"minimumWithdrawalSuffix": "Yuan Withdrawable",
|
||||
"purchaseOrderCount": "Purchase Order Count",
|
||||
"promotionLinkVisits": "Promotion Link Visits",
|
||||
"registeredUsers": "Registered Users",
|
||||
"referralEarnings": "Referral Earnings",
|
||||
"referralEarningsDescription": "Commission amount returned after referred users register and buy products",
|
||||
"percentage": "Percentage",
|
||||
"applyForAdvancedAgent": "Apply to Become an Advanced Agent",
|
||||
"contactAdminForAdvancedAgent": "Contact the site owner to apply for an advanced agent to enjoy high commissions",
|
||||
"joinAsPartner": "Join Us as a Partner",
|
||||
"partnerDescription": "Join us as a partner to co-operate the community, win-win cooperation!",
|
||||
"winTogether": "Win Together, Advance Together",
|
||||
"referralLink": "Referral Link:",
|
||||
"apply": "Apply",
|
||||
"referralRecordsTab": "Referral Records",
|
||||
"withdrawalRecordsTab": "Withdrawal Records",
|
||||
"registeredUsersTab": "Registered Users",
|
||||
"inviteFriends": "Invite friends, gift meal cards, and enjoy recharge commissions!",
|
||||
"inviteLink": "Invite Link",
|
||||
"copy": "Copy",
|
||||
"inviteBenefits1": "Both parties enjoy a certain amount of permanent card rewards when inviting friends.",
|
||||
"inviteBenefits2Prefix": "Earn a ",
|
||||
"inviteBenefits2Suffix": "% commission on your friend's recharge amount.",
|
||||
"enterWithdrawalAmount": "Please enter your withdrawal amount!",
|
||||
"selectWithdrawalChannel": "Please select your withdrawal channel!",
|
||||
"enterContactInfo": "Please provide your contact information and remark!",
|
||||
"optionalRemark": "If there are any special circumstances, please remark!",
|
||||
"withdrawalSuccess": "Withdrawal application successful, please wait for approval!",
|
||||
"withdrawalApplicationForm": "Withdrawal Application Form",
|
||||
|
||||
"contactInformation": "Contact Information",
|
||||
|
||||
"withdrawalRemark": "Withdrawal Remark",
|
||||
"enterWithdrawalRemark": "Please enter your withdrawal remarks",
|
||||
"applyWithdrawal": "Apply for Withdrawal"
|
||||
},
|
||||
"goods": {
|
||||
"purchaseSuccess": "Purchase successful, enjoy your item!",
|
||||
"paymentNotSuccessful": "You have not completed the payment yet!",
|
||||
"orderConfirmationTitle": "Order Confirmation",
|
||||
"orderConfirmationContent": "Welcome to purchase, are you sure you want to buy ",
|
||||
"thinkAgain": "Let me think again",
|
||||
"confirmPurchase": "Confirm Purchase",
|
||||
"paymentNotEnabled": "Payment has not been enabled by the administrator!",
|
||||
"selectProducts": "Select Products",
|
||||
"basicModelQuota": "Basic Model Quota",
|
||||
"advancedModelQuota": "Advanced Model Quota",
|
||||
"drawingQuota": "Drawing Quota",
|
||||
"buyPackage": "Buy Package"
|
||||
},
|
||||
"rechargeTypes": {
|
||||
"1": "Registration Bonus",
|
||||
"2": "Invitation Bonus",
|
||||
"3": "Referring Others Bonus",
|
||||
"4": "Purchase via Card Code",
|
||||
"5": "Admin Bonus",
|
||||
"6": "QR Code Purchase",
|
||||
"7": "MJ Drawing Failure Refund",
|
||||
"8": "Sign-in Reward"
|
||||
},
|
||||
"orderStatus": {
|
||||
"0": "Not Paid",
|
||||
"1": "Paid",
|
||||
"2": "Payment Failed",
|
||||
"3": "Payment Timeout"
|
||||
},
|
||||
"messages": {
|
||||
"logoutSuccess": "Successfully logged out!"
|
||||
}
|
||||
}
|
||||
50
chat/src/locales/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Language } from '@/store/modules/app/helper'
|
||||
import type { App } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import enUS from './en-US.json'
|
||||
import zhCN from './zh-CN.json'
|
||||
// import zhTW from './zh-TW.json';
|
||||
|
||||
// const defaultLocale = savedLocale || appStore.language || 'zh-CN';
|
||||
// const defaultLocale = 'en-US'
|
||||
const defaultLocale = 'zh-CN'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // 设置为false启用Composition API模式
|
||||
globalInjection: true, // 启用全局注入
|
||||
locale: defaultLocale,
|
||||
fallbackLocale: 'en-US',
|
||||
allowComposition: true,
|
||||
messages: {
|
||||
'en-US': enUS,
|
||||
'zh-CN': zhCN,
|
||||
// 'zh-TW': zhTW,
|
||||
},
|
||||
})
|
||||
|
||||
// 导出t函数以便在组件外部使用
|
||||
export function t(key: string) {
|
||||
return i18n.global.t(key)
|
||||
}
|
||||
|
||||
export function setLocale(locale: Language) {
|
||||
// 由于i18n.global.locale.value只接受特定字符串类型,需要进行类型断言
|
||||
i18n.global.locale.value = locale as 'zh-CN' | 'en-US'
|
||||
// 将新的语言设置保存到 localStorage
|
||||
localStorage.setItem('appLanguage', locale)
|
||||
}
|
||||
|
||||
// 使用加密和指定过期时间(或永久保存)保存语言设置
|
||||
// export function setLocale(locale: Language) {
|
||||
// console.log(`正在切换语言至: ${locale}`);
|
||||
// i18n.global.locale = locale;
|
||||
// // 使用自定义 localStorage 工具保存语言设置
|
||||
// ls.set('appLanguage', locale);
|
||||
// console.log(`当前语言已切换至: ${i18n.global.locale}`);
|
||||
// }
|
||||
|
||||
export function setupI18n(app: App) {
|
||||
app.use(i18n)
|
||||
}
|
||||
|
||||
export default i18n
|
||||
585
chat/src/locales/zh-CN.json
Normal file
@@ -0,0 +1,585 @@
|
||||
{
|
||||
"language": "中文",
|
||||
"common": {
|
||||
"add": "添加",
|
||||
"addSuccess": "添加成功",
|
||||
"edit": "编辑",
|
||||
"editSuccess": "编辑成功",
|
||||
"delete": "删除",
|
||||
"deleteSuccess": "删除成功",
|
||||
"update": "修改",
|
||||
"saveSuccess": "保存成功",
|
||||
"updateUserSuccess": "修改用户信息成功",
|
||||
"reset": "重置",
|
||||
"action": "操作",
|
||||
"export": "导出",
|
||||
"exportSuccess": "导出成功",
|
||||
"import": "导入",
|
||||
"importSuccess": "导入成功",
|
||||
"clear": "清空",
|
||||
"clearSuccess": "清空成功",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"download": "下载",
|
||||
"noData": "暂无数据",
|
||||
"wrong": "好像出错了,请稍后再试。",
|
||||
"success": "操作成功",
|
||||
"failed": "操作失败",
|
||||
"verify": "验证",
|
||||
"unauthorizedTips": "未经授权,请先进行验证。",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"chat": {
|
||||
"newChatButton": "新建聊天",
|
||||
"placeholder": "来说点什么吧...(Shift + Enter = 换行)",
|
||||
"placeholderMobile": "来说点什么...",
|
||||
"copy": "复制",
|
||||
"copied": "复制成功",
|
||||
"copyCode": "复制",
|
||||
"clearChat": "清空会话",
|
||||
"clearChatConfirm": "是否清空会话?",
|
||||
"exportImage": "保存会话到图片",
|
||||
"exportImageConfirm": "是否将会话保存为图片?",
|
||||
"exportSuccess": "保存成功",
|
||||
"exportFailed": "保存失败",
|
||||
"deleteMessage": "删除消息",
|
||||
"deleteMessageConfirm": "删除此条对话?",
|
||||
"deleteHistoryConfirm": "确定删除此记录?",
|
||||
"deleteSuccess": "删除成功",
|
||||
"clearHistoryConfirm": "确定清空聊天记录?",
|
||||
"preview": "预览",
|
||||
"showRawText": "显示原文",
|
||||
"size": "尺寸:",
|
||||
"generatedContentDisclaimer": "AI 生成内容仅供参考,不代表本平台立场。版权所有 ©",
|
||||
"square1": "方形(1:1)",
|
||||
"illustration": "配图(4:3)",
|
||||
"wallpaper": "壁纸(16:9)",
|
||||
"media": "媒体(3:4)",
|
||||
"poster": "海报(9:16)",
|
||||
"square": "方形",
|
||||
"landscape": "宽屏",
|
||||
"portrait": "垂直",
|
||||
"chatDialogue": "对话聊天",
|
||||
"startNewConversationPrompt": "点击下方按钮,开始一个新的对话吧",
|
||||
"newConversation": "新对话",
|
||||
"networkModeEnabledContextInvalid": "已开启联网模式、上下文状态失效!",
|
||||
"networkModeDisabled": "已关闭联网模式!",
|
||||
"pointsMall": "积分商城",
|
||||
"toggleTheme": "切换主题",
|
||||
"signInReward": "签到奖励",
|
||||
"networkMode": "联网模式",
|
||||
"searchHistoryConversations": "搜索历史对话",
|
||||
"announcement": "网站公告",
|
||||
"clear": "清空对话",
|
||||
"remaining": "剩余:",
|
||||
"ordinaryPoints": "普通积分",
|
||||
"advancedPoints": "高级积分",
|
||||
"drawingPoints": "绘画积分",
|
||||
"points": "积分",
|
||||
"clearConversation": "清空对话",
|
||||
"clearAllNonFavoriteConversations": "清空所有非收藏的对话?",
|
||||
"more": "更多",
|
||||
"collapse": "折叠",
|
||||
"myApps": "我的应用",
|
||||
"appSquare": "应用广场",
|
||||
"favorites": "收藏",
|
||||
"todayConversations": "今日对话",
|
||||
"historyConversations": "历史对话",
|
||||
"favoriteConversations": "收藏对话",
|
||||
"unfavorite": "取消收藏",
|
||||
"rename": "重命名",
|
||||
"deleteConversation": "删除对话",
|
||||
"me": "我",
|
||||
"onlineSearch": "联网搜索",
|
||||
"mindMap": "思维导图",
|
||||
"fileAnalysis": "文件",
|
||||
"delete": "删除",
|
||||
"regenerate": "重新生成",
|
||||
"pause": "暂停",
|
||||
"loading": "加载中...",
|
||||
"readAloud": "朗读",
|
||||
"vipCenter": "会员中心",
|
||||
"U1": "🔍 放大左上",
|
||||
"U2": "🔍 放大右上",
|
||||
"U3": "🔍 放大左下",
|
||||
"U4": "🔍 放大右下",
|
||||
"V1": "🪄 变换左上",
|
||||
"V2": "🪄 变换右上",
|
||||
"V3": "🪄 变换左下",
|
||||
"V4": "🪄 变换右下",
|
||||
"panLeft": "⬅️ 向左平移",
|
||||
"panRight": "➡️ 向右平移",
|
||||
"panUp": "⬆️ 向上平移",
|
||||
"panDown": "⬇️ 向下平移",
|
||||
"zoomIn15x": "↔️ 扩图1.5倍",
|
||||
"zoomIn2x": "↔️ 扩图2倍",
|
||||
"minorTransform": "🖌️ 微变换",
|
||||
"strongTransform": "🖌️ 强变换",
|
||||
"enlargeImagePrefix": "放大第",
|
||||
"enlargeImageSuffix": "张图片",
|
||||
"transformImagePrefix": "变换第",
|
||||
"transformImageSuffix": "张图片",
|
||||
"expandDrawing": "扩图绘制",
|
||||
"advancedTransform": "高级变换",
|
||||
"translateImage": "平移图片",
|
||||
"imageToImage": "以图生图",
|
||||
"faceConsistency": "人脸一致",
|
||||
"styleConsistency": "风格一致",
|
||||
"selectAppOrTopic": "选择应用或话题快速对话"
|
||||
},
|
||||
"app": {
|
||||
"sampleTemplate": "示例模板",
|
||||
"exploreInfinitePossibilities": "探索无限可能,与 AI 一同开创智慧未来",
|
||||
"searchAppNameQuickFind": "搜索应用名称、快速查找应用...",
|
||||
"allCategories": "全部分类",
|
||||
"noModelConfigured": "管理员未配置特定应用模型、请联系管理员配置~"
|
||||
},
|
||||
"setting": {
|
||||
"setting": "设置",
|
||||
"general": "总览",
|
||||
"advanced": "高级",
|
||||
"personalInfo": "个人信息",
|
||||
"avatarLink": "头像链接",
|
||||
"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 文件有效性"
|
||||
},
|
||||
"draw": {
|
||||
"use": "使用",
|
||||
"download": "下载",
|
||||
"delete": "删除",
|
||||
"zoom": "放大:",
|
||||
"U1": "左上",
|
||||
"U2": "右上",
|
||||
"U3": "左下",
|
||||
"U4": "右下",
|
||||
"regenerateOnce": "重新生成一次",
|
||||
"transform": "变换:",
|
||||
"V1": "左上",
|
||||
"V2": "右上",
|
||||
"V3": "左下",
|
||||
"V4": "右下",
|
||||
"pan": "平移:",
|
||||
"panLeft": "向左",
|
||||
"panRight": "向右",
|
||||
"panUp": "向上",
|
||||
"panDown": "向下",
|
||||
"transformZoom": "扩图变换:",
|
||||
"zoom1_5x": "扩图1.5倍",
|
||||
"zoom2x": "扩图2倍",
|
||||
"minorTransform": "微变换",
|
||||
"strongTransform": "强变换",
|
||||
"regionalRedraw": "区域重绘",
|
||||
"regionalRedraw1": "区域重绘(框选要改变的区域)",
|
||||
"submitTask": "提交任务",
|
||||
"selectSuiteForZoom": "操作:选中套图进行放大",
|
||||
"selectSuiteForTransform": "操作:选中套图进行变换",
|
||||
"regeneratingImage": "操作:正在对图片重新生成一次",
|
||||
"drawingInProgress": "操作:正在火速绘制中...",
|
||||
"tryDifferentPrompt": "执行:换个提示词重新试试吧!",
|
||||
"statusWaiting": "等待中",
|
||||
"statusDrawing": "绘制中",
|
||||
"statusSuccess": "成功",
|
||||
"statusFailure": "失败",
|
||||
"statusTimeout": "超时",
|
||||
"downloadImageTitle": "下载图片",
|
||||
"downloadImageContent": "下载当前图片",
|
||||
"downloadButtonText": "下载",
|
||||
"cancelButtonText": "取消",
|
||||
"deleteRecordTitle": "删除记录",
|
||||
"deleteRecordContent": "删除当前绘制记录?",
|
||||
"deleteButtonText": "删除",
|
||||
"submitZoomDrawingSuccess": "提交放大绘制任务成功、请等待绘制结束!",
|
||||
"submitRedrawSuccess": "提交重新绘制任务成功、请等待绘制结束!",
|
||||
"submitTransformDrawingSuccess": "提交变换绘制任务成功、请等待绘制结束!",
|
||||
"submitEnlargeDrawingSuccess": "提交扩图任务成功、请等待绘制结束!",
|
||||
"submitAdvancedTransformDrawingSuccess": "提交高级变换绘制任务成功、请等待绘制结束!",
|
||||
"submitRegionalRedrawSuccess": "提交区域重绘任务成功、请等待绘制结束!",
|
||||
"drawingRecordDeleted": "绘制记录已删除!",
|
||||
"queueing": "排队中...",
|
||||
"drawing": "正在绘制...",
|
||||
"storing": "图片存储中...",
|
||||
"drawingFailed": "绘制失败",
|
||||
"pointsRefunded": "积分已退还!",
|
||||
"submitDrawingTaskSuccess": "提交绘制任务成功、请等待绘制结束!",
|
||||
"defaultStyle": "默认风格",
|
||||
"expressiveStyle": "表现力风格",
|
||||
"cuteStyle": "可爱风格",
|
||||
"scenicStyle": "景观风格",
|
||||
"standardQuality": "普通",
|
||||
"generalQuality": "一般",
|
||||
"highDefinitionQuality": "高清",
|
||||
"ultraHighDefinitionQuality": "超高清",
|
||||
"enterDescription": "请输入描述词!",
|
||||
"optimizationFailed": "优化失败了!",
|
||||
"professionalDrawing": "专业绘图",
|
||||
"parameterExplanation": "参数释义:生成图片尺寸比例",
|
||||
"imageSize": "图片尺寸",
|
||||
"modelSelection": "模型选择",
|
||||
"tooltipMJ": "MJ: 偏真实通用模型",
|
||||
"tooltipNIJI": "NIJI: 偏动漫风格、适用于二次元模型",
|
||||
"version": "版本",
|
||||
"style": "风格",
|
||||
"parameters": "参数",
|
||||
"parametersTooltip": "合理使用参数绘制更为理想的结果!",
|
||||
"quality": "品质",
|
||||
"chaos": "混乱",
|
||||
"chaosDescription": "取值范围:0-100、 --chaos 或 --c",
|
||||
"chaosExplanation": "混乱级别,可以理解为让AI天马行空的空间",
|
||||
"chaosAdvice": "值越小越可靠、默认0最为精准",
|
||||
"stylization": "风格化",
|
||||
"stylizationDescription": "风格化:--stylize 或 --s,范围 1-1000",
|
||||
"parameterExplanation1": "参数释义:数值越高,画面表现也会更具丰富性和艺术性",
|
||||
"setting": "设定",
|
||||
"carryParameters": "携带参数",
|
||||
"autoCarryParameters": "是否自动携带参数",
|
||||
"carryOn": "打开:携带上述我们配置的参数",
|
||||
"carryOff": "关闭:使用指令中的我们自定义的参数",
|
||||
"imageToImage": "以图生图",
|
||||
"clickOrDrag": "点击或拖拽一个图片到这里作为输入",
|
||||
"supportFormats": "支持PNG和JPG格式",
|
||||
"remainingPoints": "剩余积分",
|
||||
"refresh": "刷新",
|
||||
"accountInfo": "账户信息",
|
||||
"points": "积分",
|
||||
"paintingSingleUse": "绘画单次消耗:",
|
||||
"imageGenerationSingleUse": "图生图单次消耗:",
|
||||
"enlargementSingleUse": "放大单次消耗:",
|
||||
"submitDrawingTask": "输入关键词,提交绘制任务",
|
||||
"optimize": "优化",
|
||||
"enterDrawingKeywords": "输入绘图关键词。例如:一只五颜六色的猫,可爱,卡通",
|
||||
"unnecessaryElements": "不需要的元素",
|
||||
"exclusionPrompt": "例:生成房间图片、但是不要床、你可以填bed!",
|
||||
"workingContents": "工作中的内容",
|
||||
"currentTasks": "当前系统进行中任务",
|
||||
"goToAIDrawingSquare": "点击前往 AI 绘画广场",
|
||||
"tasksInProgress": "个任务正在进行中、请耐心等候绘制完成、您可以前往其他页面稍后回来查看结果!",
|
||||
"myDrawings": "我的绘图",
|
||||
"aiDrawingSquare": "AI绘画广场",
|
||||
"sizeAdjustment": "尺寸调整",
|
||||
"keywordSearchPlaceholder": "prompt关键词搜索"
|
||||
},
|
||||
"pay": {
|
||||
"membershipMarket": "会员商场",
|
||||
"sizeAdjustment": "尺寸调整",
|
||||
"memberPackage": "会员限时套餐",
|
||||
"permanentAddOnCard": "叠加永久次卡",
|
||||
"baseModelQuota": "普通积分",
|
||||
"advancedModelQuota": "高级积分",
|
||||
"MJDrawingQuota": "绘画积分",
|
||||
"packageValidity": "套餐有效期",
|
||||
"days": "天",
|
||||
"permanent": "永久",
|
||||
"points": "积分",
|
||||
"welcomeTipMobile": "尽情探索,欢迎光临我们的在线商店!",
|
||||
"welcomeTipDesktop": "尽情探索,欢迎光临我们的在线商店、感谢您选择我们、让我们一同开启愉悦的购物之旅!",
|
||||
"paymentNotEnabled": "管理员还未开启支付!",
|
||||
"purchaseSuccess": "购买成功、祝您使用愉快!",
|
||||
"paymentNotComplete": "您还没有支付成功哟!",
|
||||
"wechat": "微信",
|
||||
"alipay": "支付宝",
|
||||
"wechatPay": "微信支付",
|
||||
"alipayPay": "支付宝支付",
|
||||
"paymentSuccess": "恭喜你支付成功、祝您使用愉快!",
|
||||
"paymentTimeout": "支付超时,请重新下单!",
|
||||
"productPayment": "商品支付",
|
||||
"amountDue": "需要支付:",
|
||||
"packageName": "套餐名称:",
|
||||
"packageDescription": "套餐描述:",
|
||||
"siteAdminEnabledRedirect": "当前站长开通了跳转支付",
|
||||
"clickToPay": "点击前往支付",
|
||||
"completePaymentWithin": "请在",
|
||||
"timeToCompletePayment": "时间内完成支付!",
|
||||
"open": "打开",
|
||||
"scanToPay": "扫码支付"
|
||||
},
|
||||
"mindmap": {
|
||||
"title": "思维导图",
|
||||
"yourNeeds": "您的需求?",
|
||||
"inputPlaceholder": "请输入您想要生成内容的简单描述、AI将为您输出一份完整的markdown内容及其思维导图!",
|
||||
"generateMindMapButton": "智能生成生成思维导图",
|
||||
"contentRequirements": "内容需求",
|
||||
"tryDemoButton": "试试示例",
|
||||
"usageCredits": "每次使用消耗基础积分: 1",
|
||||
"exportHTML": "导出HTML",
|
||||
"exportPNG": "导出PNG",
|
||||
"exportSVG": "导出SVG"
|
||||
},
|
||||
"usercenter": {
|
||||
"defaultSignature": "我是一台基于深度学习和自然语言处理技术的 AI 机器人,旨在为用户提供高效、精准、个性化的智能服务。",
|
||||
"syncComplete": "已同步数据完成",
|
||||
"personalCenter": "个人中心",
|
||||
"logOut": "退出登录",
|
||||
"myUsageRecord": "我在本站的使用记录",
|
||||
"basicModelCredits": "基础模型积分:",
|
||||
"advancedModelCredits": "高级模型积分:",
|
||||
"basicModelUsage": "基础模型使用:",
|
||||
"advancedModelUsage": "高级模型使用:",
|
||||
"drawingUsageCredits": "绘画使用积分:",
|
||||
"bindWeChat": "绑定微信:",
|
||||
"clickToBindWeChat": "点击绑定微信",
|
||||
"weChatBound": "已绑定微信",
|
||||
"syncVisitorData": "点击同步访客数据",
|
||||
"points": "积分",
|
||||
"membershipExpiration": "会员过期时间:",
|
||||
"editInfoDescription": "编辑个人信息、查看更多详情",
|
||||
"myDetails": "我的详情",
|
||||
"myWallet": "我的钱包",
|
||||
"basicInfo": "基础信息",
|
||||
"userBasicSettings": "用户基础设置",
|
||||
"avatarPlaceholder": "请填写头像地址",
|
||||
"usernamePlaceholder": "请编辑您的用户名",
|
||||
"signaturePlaceholder": "请编辑您的签名",
|
||||
"passwordManagement": "密码管理",
|
||||
"inviteBenefits": "邀请得福利",
|
||||
"clickToLogin": "点击登入",
|
||||
"notLoggedIn": "未登录",
|
||||
"avatar": "头像",
|
||||
"username": "用户名称",
|
||||
"email": "用户邮箱",
|
||||
"inviteeStatus": "受邀人状态",
|
||||
"inviteTime": "邀请时间",
|
||||
"rewardStatus": "获得奖励状态",
|
||||
"certified": "已认证",
|
||||
"notActivated": "未激活",
|
||||
"rewardReceived": "已领取邀请奖励",
|
||||
"waitingConfirmation": "等待受邀人确认",
|
||||
"linkGeneratedSuccess": "生成邀请链接成功",
|
||||
"generateLinkFirst": "请先生成您的专属邀请链接!",
|
||||
"linkCopiedSuccess": "复制专属邀请链接成功!",
|
||||
"copyNotSupported": "当前设置不支持自动复制、手动复制吧!",
|
||||
"inviteForBenefits": "邀用户、得福利!",
|
||||
"myInviteCode": "我的邀请码",
|
||||
"generateInviteCode": "生成专属邀请码",
|
||||
"copyInviteLink": "复制专属邀请链接",
|
||||
"inviteOneUser": "邀请一位用户赠送",
|
||||
"basicModelCredits1": "积分基础模型额度+",
|
||||
"advancedModelCredits1": "积分高级模型额度+",
|
||||
"mjDrawingCredits": "MJ绘画积分额度",
|
||||
"receiveInvitation": "收到邀请用户获得",
|
||||
"creditsEnd": "积分",
|
||||
"invitationRecord": "邀请记录",
|
||||
"passwordMinLength": "密码最短长度为6位数",
|
||||
"passwordMaxLength": "密码最长长度为30位数",
|
||||
"enterPassword": "请输入密码",
|
||||
"reenterPassword": "请再次输入密码",
|
||||
"passwordsNotMatch": "两次密码输入不一致",
|
||||
"passwordUpdateSuccess": "密码更新成功、请重新登录系统!",
|
||||
"changeYourPassword": "变更您的密码",
|
||||
"oldPassword": "旧密码",
|
||||
"newPassword": "新密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"reloginAfterPasswordChange": "更新密码完成后将重新登录!",
|
||||
"updateYourPassword": "更新您的密码",
|
||||
"passwordRequirements": "密码要求",
|
||||
"newPasswordInstructions": "要创建一个新的密码,你必须满足以下所有要求。",
|
||||
"minimumCharacters": "最少6个字符",
|
||||
"maximumCharacters": "最多30个字符",
|
||||
"requireNumber": "至少带有一个数字",
|
||||
"orderNumber": "订单编号",
|
||||
"rechargeType": "充值类型",
|
||||
"basicModelQuota": "普通积分",
|
||||
"advancedModelQuota": "高级积分",
|
||||
"mjDrawingQuota": "绘画积分",
|
||||
"validity": "有效期",
|
||||
"rechargeTime": "充值时间",
|
||||
"enterCardSecret": "请先填写卡密!",
|
||||
"cardRedeemSuccess": "卡密兑换成功、祝您使用愉快!",
|
||||
"userWalletBalance": "用户钱包余额",
|
||||
"basicModelBalance": "基础模型余额",
|
||||
"creditUsageNote": "每次对话根据模型消费不同积分!",
|
||||
"advancedModelBalance": "高级模型余额",
|
||||
"modelConsumptionNote": "每次对话根据模型消费不同积分!",
|
||||
"mjDrawingBalance": "MJ绘画余额",
|
||||
"drawingConsumptionNote": "根据画图动作消耗不同的积分!",
|
||||
"cardRecharge": "卡密充值",
|
||||
"enterCardDetails": "请粘贴或填写您的卡密信息!",
|
||||
"pleaseEnterCardDetails": "请输入卡密信息",
|
||||
"exchange": "兑换",
|
||||
"buyCardSecret": "购买卡密",
|
||||
"rechargeRecords": "充值记录",
|
||||
"packagePurchase": "套餐购买",
|
||||
"buyPackage": "购买套餐"
|
||||
},
|
||||
"siderBar": {
|
||||
"signInReward": "签到奖励",
|
||||
"themeSwitch": "主题切换",
|
||||
"personalCenter": "个人中心",
|
||||
"loginAccount": "登录账户"
|
||||
},
|
||||
"notice": {
|
||||
"doNotRemind24h": "我已知晓"
|
||||
},
|
||||
"login": {
|
||||
"enterUsername": "请输入用户名",
|
||||
"usernameLength": "用户名长度应为 2 到 30 个字符",
|
||||
"enterPassword": "请输入密码",
|
||||
"passwordLength": "密码长度应为 6 到 30 个字符",
|
||||
"enterEmail": "请输入邮箱地址",
|
||||
"enterPhone": "请输入手机号码",
|
||||
"enterEmailOrPhone": "请输入邮箱地址或手机号码",
|
||||
"emailValid": "请输入正确的邮箱地址",
|
||||
"enterCaptcha": "请填写图形验证码",
|
||||
"emailPhone": "邮箱 / 手机号",
|
||||
"email": "邮箱",
|
||||
"phone": "手机号",
|
||||
"registrationSuccess": "账户注册成功、开始体验吧!",
|
||||
"loginSuccess": "账户登录成功、开始体验吧!",
|
||||
"registerTitle": "注册",
|
||||
"enterContact": "请填写您的",
|
||||
"enterCode": "请填写验证码",
|
||||
"sendVerificationCode": "发送验证码",
|
||||
"optionalInvitationCode": "邀请码[非必填]",
|
||||
"registerAccount": "注册账户",
|
||||
"alreadyHaveAccount": "已经有帐号?",
|
||||
"goToLogin": "去登录",
|
||||
"password": "密码",
|
||||
"enterYourPassword": "请输入您的账户密码",
|
||||
"rememberAccount": "记住帐号",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"loginAccount": "登录账户",
|
||||
"noAccount": "还没有帐号?",
|
||||
"register": "去注册",
|
||||
"orUse": "或使用",
|
||||
"scanLogin": "扫码登录",
|
||||
"wechatLogin": "微信登录",
|
||||
"wechatScanFailed": "不使用微信扫码登录?试试",
|
||||
"useWechatScan": "使用微信扫码登录"
|
||||
},
|
||||
"share": {
|
||||
"orderAmount": "订单金额",
|
||||
"productType": "商品类型",
|
||||
"status": "状态",
|
||||
"commissionRate": "佣金比例",
|
||||
"commission": "佣金",
|
||||
"orderTime": "订购时间",
|
||||
"purchasePackage": "购买套餐",
|
||||
"accounted": "已入账",
|
||||
"generateInviteCodeSuccess": "生成邀请码成功",
|
||||
"withdrawalTime": "提现时间",
|
||||
"withdrawalAmount": "提现金额",
|
||||
"withdrawalChannel": "提现渠道",
|
||||
"withdrawalStatus": "提现状态",
|
||||
"withdrawalRemarks": "提现备注",
|
||||
"auditor": "审核人",
|
||||
"alipay": "支付宝",
|
||||
"wechat": "微信",
|
||||
"paid": "已打款",
|
||||
"rejected": "被拒绝",
|
||||
"inReview": "审核中",
|
||||
"avatar": "头像",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"inviteeStatus": "受邀人状态",
|
||||
"registered": "已注册",
|
||||
"pendingActivation": "待激活",
|
||||
"registrationTime": "注册时间",
|
||||
"lastLogin": "最后登录",
|
||||
"requestInviteCodeFirst": "请先申请你的邀请码",
|
||||
"linkCopiedSuccess": "复制推荐链接成功",
|
||||
"title": "推介计划",
|
||||
"description": "加入我们,共享成功!欢迎来到我们的分销页面,成为我们的合作伙伴,一同开创美好未来!",
|
||||
"defaultSalesOutletName": "新秀推荐官",
|
||||
"myReferrals": "我的推介",
|
||||
"currencyUnit": "元",
|
||||
"remainingAmount": "剩余可提金额",
|
||||
"withdrawingAmount": "提现中金额",
|
||||
"withdrawNow": "立即提现",
|
||||
"minimumWithdrawalPrefix": "最低",
|
||||
"minimumWithdrawalSuffix": "元可提现",
|
||||
"purchaseOrderCount": "购买订单数量",
|
||||
"promotionLinkVisits": "推广链接访问次数",
|
||||
"registeredUsers": "注册用户",
|
||||
"referralEarnings": "推介收益",
|
||||
"referralEarningsDescription": "推介的用户注册购买产品后返佣金额",
|
||||
"percentage": "百分比",
|
||||
"applyForAdvancedAgent": "申请成为高级代理",
|
||||
"contactAdminForAdvancedAgent": "联系站长申请高级代理可享超高返佣",
|
||||
"joinAsPartner": "加入我们成为合伙人",
|
||||
"partnerDescription": "加入我们成为合伙人共同运营社区、合作双赢!",
|
||||
"winTogether": "合作共赢,携手共进",
|
||||
"referralLink": "推荐链接:",
|
||||
"apply": "申请",
|
||||
"referralRecordsTab": "推介记录",
|
||||
"withdrawalRecordsTab": "提现记录",
|
||||
"registeredUsersTab": "注册用户",
|
||||
"inviteFriends": "邀好友、赠套餐卡密、享充值返佣!",
|
||||
"inviteLink": "邀请链接",
|
||||
"copy": "复制",
|
||||
"inviteBenefits1": "邀请好友双方都可享受一定额度的永久次卡奖励",
|
||||
"inviteBenefits2Prefix": "邀请好友充值,您可获得充值金额的",
|
||||
"inviteBenefits2Suffix": "%返佣",
|
||||
"enterWithdrawalAmount": "请填写你的提款金额!",
|
||||
"selectWithdrawalChannel": "请选择你的提款渠道!",
|
||||
"enterContactInfo": "请填写您的联系方式并备注!",
|
||||
"optionalRemark": "如有特殊情况、请备注说明!",
|
||||
"withdrawalSuccess": "申请提现成功、请耐心等待审核!",
|
||||
"withdrawalApplicationForm": "提款申请表",
|
||||
"contactInformation": "联系方式",
|
||||
"withdrawalRemark": "提款备注",
|
||||
"enterWithdrawalRemark": "请填写你的提款备注!",
|
||||
"applyWithdrawal": "申 请 提 现"
|
||||
},
|
||||
"goods": {
|
||||
"purchaseSuccess": "购买成功、祝您使用愉快!",
|
||||
"paymentNotSuccessful": "您还没有支付成功哟!",
|
||||
"orderConfirmationTitle": "订单确认",
|
||||
"orderConfirmationContent": "欢迎选购、确定购买",
|
||||
"thinkAgain": "我再想想",
|
||||
"confirmPurchase": "确认购买",
|
||||
"paymentNotEnabled": "管理员还未开启支付!",
|
||||
"selectProducts": "选购套餐",
|
||||
"basicModelQuota": "基础积分",
|
||||
"advancedModelQuota": "高级积分",
|
||||
"drawingQuota": "绘画积分",
|
||||
"buyPackage": "购买套餐"
|
||||
},
|
||||
"rechargeTypes": {
|
||||
"1": "注册赠送",
|
||||
"2": "受邀请赠送",
|
||||
"3": "邀请他人赠送",
|
||||
"4": "购买卡密充值",
|
||||
"5": "管理员赠送",
|
||||
"6": "扫码购买充值",
|
||||
"7": "MJ绘画失败退款",
|
||||
"8": "签到奖励"
|
||||
},
|
||||
"orderStatus": {
|
||||
"0": "未支付",
|
||||
"1": "已支付",
|
||||
"2": "支付失败",
|
||||
"3": "支付超时"
|
||||
},
|
||||
"messages": {
|
||||
"logoutSuccess": "登出账户成功!"
|
||||
}
|
||||
}
|
||||
103
chat/src/locales/zh-TW.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"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": "顯示原文",
|
||||
"loading": "加載中...",
|
||||
"readAloud": "朗讀",
|
||||
"vipCenter": "會員中心"
|
||||
},
|
||||
"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 檔案有效性"
|
||||
},
|
||||
"usercenter": {
|
||||
"enterCardSecret": "請先填寫卡密!",
|
||||
"cardRedeemSuccess": "卡密兌換成功、祝您使用愉快!",
|
||||
"cardRecharge": "卡密充值",
|
||||
"enterCardDetails": "請粘貼或填寫您的卡密資訊!",
|
||||
"pleaseEnterCardDetails": "請輸入卡密資訊",
|
||||
"exchange": "兌換",
|
||||
"buyCardSecret": "購買卡密"
|
||||
}
|
||||
}
|
||||
84
chat/src/main.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import { useAuthStoreWithout } from '@/store/modules/auth'
|
||||
import '@/styles/github-markdown.less'
|
||||
import '@/styles/global.less'
|
||||
// import '@/styles/highlight.less' // 移除旧的highlight样式
|
||||
import '@/styles/index.css'
|
||||
import { print99aiInfo, printAppInfo } from '@/utils/logger'
|
||||
import { message } from '@/utils/message'
|
||||
import router from '@/utils/router'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { setupI18n } from './locales'
|
||||
import { setupImageViewer } from './plugins/imageViewer'
|
||||
import { setupStore } from './store'
|
||||
|
||||
// 禁用生产环境中的 console.log
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log = () => {}
|
||||
console.warn = () => {}
|
||||
console.error = () => {}
|
||||
}
|
||||
|
||||
// 检测系统主题并设置应用主题
|
||||
function detectSystemTheme() {
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const storedTheme = localStorage.getItem('theme')
|
||||
const theme = storedTheme || (prefersDarkScheme.matches ? 'dark' : 'light')
|
||||
localStorage.setItem('theme', theme)
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark')
|
||||
|
||||
// 设置 data-theme 方式的主题系统
|
||||
document.documentElement.dataset.theme = theme
|
||||
}
|
||||
|
||||
const authStore = useAuthStoreWithout()
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App)
|
||||
|
||||
// 设置样式和资源
|
||||
setupStore(app)
|
||||
setupI18n(app)
|
||||
setupImageViewer(app)
|
||||
|
||||
// 安装Vue Router
|
||||
app.use(router)
|
||||
|
||||
// 检测系统主题并设置应用主题
|
||||
detectSystemTheme()
|
||||
|
||||
// 初始化主题
|
||||
const { init } = useTheme()
|
||||
init()
|
||||
|
||||
// 初始化消息组件
|
||||
const msgInstance = message()
|
||||
|
||||
// 在开发环境下打印控制台信息
|
||||
print99aiInfo()
|
||||
printAppInfo('99AI', '5.0.1')
|
||||
|
||||
const domain = `${window.location.protocol}//${window.location.hostname}${
|
||||
window.location.port ? `:${window.location.port}` : ''
|
||||
}`
|
||||
await authStore.getGlobalConfig(domain)
|
||||
|
||||
// 延迟加载插件
|
||||
const VueViewer = (await import('v-viewer')).default
|
||||
app.use(VueViewer)
|
||||
const { MotionPlugin } = await import('@vueuse/motion')
|
||||
app.use(MotionPlugin)
|
||||
|
||||
// 在卸载应用前清理资源
|
||||
app.config.globalProperties.$onAppUnmount = () => {
|
||||
if (msgInstance && typeof msgInstance.destroy === 'function') {
|
||||
msgInstance.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
11
chat/src/plugins/imageViewer.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import GlobalImageViewer from '@/components/common/ImageViewer/GlobalImageViewer.vue'
|
||||
import ImageViewerPlugin from '@/components/common/ImageViewer/useImageViewer'
|
||||
import { App } from 'vue'
|
||||
|
||||
export function setupImageViewer(app: App) {
|
||||
// 安装图片预览器插件
|
||||
app.use(ImageViewerPlugin)
|
||||
|
||||
// 注册全局图片预览器组件
|
||||
app.component('GlobalImageViewer', GlobalImageViewer)
|
||||
}
|
||||
80
chat/src/services/wechatLogin.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { fetchLoginByCodeAPI, fetchWxLoginRedirectAPI } from '@/api/user'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 全局作用域不使用useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.isLogin)
|
||||
|
||||
// 微信登录逻辑
|
||||
export async function loginByWechat() {
|
||||
if (isLogin.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 在函数内部使用useRouter
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
// alert(urlParams);
|
||||
|
||||
const codes = urlParams.getAll('code')
|
||||
const code = codes.length > 0 ? codes[codes.length - 1] : null
|
||||
|
||||
if (code) {
|
||||
try {
|
||||
const res = await fetchLoginByCodeAPI<any>({ code: code })
|
||||
if (res.success) {
|
||||
authStore.setToken(res.data)
|
||||
await authStore.getUserInfo()
|
||||
authStore.setLoginDialog(false) // 关闭登录对话框
|
||||
// return { success: true };
|
||||
router.replace('/')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('微信登录失败', error)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const currentUrl = window.location.href
|
||||
// const currentUrl = window.location.href.split('#')[0];
|
||||
const res = await fetchWxLoginRedirectAPI<any>({ url: currentUrl })
|
||||
// alert(res.data);
|
||||
if (res.success) {
|
||||
// alert(window.location.href);
|
||||
window.location.replace(res.data)
|
||||
// window.location.href = res.data; // 跳转到微信授权页面
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取微信授权链接失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 检测当前浏览器是否是微信内置浏览器
|
||||
// * 区分微信和企业微信,只有微信浏览器返回true
|
||||
// * @returns {boolean} 如果是微信浏览器返回 true,否则返回 false
|
||||
// */
|
||||
// export function isWechatBrowser(): boolean {
|
||||
// const ua = navigator.userAgent.toLowerCase();
|
||||
|
||||
// // 检查是否是企业微信
|
||||
// const isWXWork = ua.indexOf('wxwork') !== -1;
|
||||
|
||||
// // 检查是否是微信,排除企业微信的情况
|
||||
// const isWeixin = !isWXWork && ua.indexOf('micromessenger') !== -1;
|
||||
|
||||
// return isWeixin;
|
||||
// }
|
||||
|
||||
// 初始化微信登录
|
||||
export function initWechatLogin() {
|
||||
// if (isWechatBrowser()) {
|
||||
loginByWechat() // 执行微信登录逻辑
|
||||
// }
|
||||
}
|
||||
10
chat/src/store/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import type { App } from 'vue'
|
||||
|
||||
export const store = createPinia()
|
||||
|
||||
export function setupStore(app: App) {
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
export * from './modules'
|
||||
44
chat/src/store/modules/app/helper.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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' | 'wechat' | 'web' | 'mobile'
|
||||
|
||||
export interface AppState {
|
||||
siderCollapsed: boolean
|
||||
theme: Theme
|
||||
language: Language
|
||||
env: Env
|
||||
}
|
||||
|
||||
export function defaultSetting(): AppState {
|
||||
return {
|
||||
siderCollapsed: false,
|
||||
theme: 'light',
|
||||
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)
|
||||
}
|
||||
59
chat/src/store/modules/app/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { store } from '@/store'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AppState, Language, Theme } from './helper'
|
||||
import { getLocalSetting, setLocalSetting } from './helper'
|
||||
|
||||
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()
|
||||
// 切换暗黑模式逻辑
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
},
|
||||
|
||||
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)
|
||||
}
|
||||
21
chat/src/store/modules/appStore/helper.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface MineApp {
|
||||
userId: number
|
||||
appId: number
|
||||
public: boolean
|
||||
status: number
|
||||
demoData: string
|
||||
order: number
|
||||
appDes: string
|
||||
preset: string
|
||||
appRole: string
|
||||
coverImg: string
|
||||
appName: string
|
||||
loading?: boolean
|
||||
backgroundImg?: string
|
||||
prompt?: string
|
||||
}
|
||||
|
||||
export interface AppStoreState {
|
||||
catId: number
|
||||
mineApps: MineApp[]
|
||||
}
|
||||
26
chat/src/store/modules/appStore/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { fetchQueryMineAppsAPI } from '@/api/appStore'
|
||||
import { store } from '@/store'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AppStoreState } from './helper'
|
||||
|
||||
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 useAppCatStore(store)
|
||||
}
|
||||
140
chat/src/store/modules/auth/helper.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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
|
||||
siteUrl: string
|
||||
qqNumber: string
|
||||
vxNumber: string
|
||||
baiduCode: string
|
||||
buyCramiAddress: string
|
||||
noticeInfo: string
|
||||
registerSendStatus: string
|
||||
registerSendModel3Count: string
|
||||
registerSendModel4Count: string
|
||||
registerSendDrawMjCount: string
|
||||
clientHomePath: string
|
||||
clientLogoPath: string
|
||||
enableHtmlRender: string
|
||||
clientFaviconPath: string
|
||||
isUseWxLogin: boolean
|
||||
robotAvatar: string
|
||||
siteRobotName: string
|
||||
mindDefaultData: string
|
||||
payEpayStatus: string
|
||||
payDuluPayStatus: string
|
||||
payHupiStatus: string
|
||||
payWechatStatus: string
|
||||
payEpayChannel: string
|
||||
payDuluPayChannel: string
|
||||
payDuluPayRedirect: string
|
||||
payHupiChannel: string
|
||||
payWechatChannel: string
|
||||
payEpayApiPayUrl: string
|
||||
payMpayStatus: string
|
||||
payMpayChannel: string
|
||||
isAutoOpenNotice: string
|
||||
isShowAppCatIcon: string
|
||||
salesBaseRatio: string
|
||||
salesSeniorRatio: string
|
||||
salesAllowDrawMoney: string
|
||||
companyName: string
|
||||
filingNumber: string
|
||||
emailLoginStatus: string
|
||||
phoneLoginStatus: string
|
||||
openIdentity: string
|
||||
openPhoneValidation: string
|
||||
wechatRegisterStatus: string
|
||||
wechatSilentLoginStatus: string
|
||||
oldWechatMigrationStatus: string
|
||||
officialOldAccountSuccessText: string
|
||||
officialOldAccountFailText: string
|
||||
signInStatus: string
|
||||
signInModel3Count: string
|
||||
signInModel4Count: string
|
||||
signInMjDrawToken: string
|
||||
appMenuHeaderTips: string
|
||||
appMenuHeaderBgUrl: string
|
||||
pluginFirst: string
|
||||
mjHideNotBlock: string
|
||||
mjUseBaiduFy: string
|
||||
mjHideWorkIn: string
|
||||
isVerifyEmail: string
|
||||
payLtzfStatus: string
|
||||
drawingStyles: string
|
||||
isHidePlugin: string
|
||||
showWatermark: string
|
||||
isHideTts: string
|
||||
isHideDefaultPreset: string
|
||||
isHideModel3Point: string
|
||||
isHideModel4Point: string
|
||||
isHideDrawMjPoint: string
|
||||
model3Name: string
|
||||
model4Name: string
|
||||
drawMjName: string
|
||||
isModelInherited: string
|
||||
noVerifyRegister: string
|
||||
homeHtml: string
|
||||
isAutoOpenAgreement: string
|
||||
agreementInfo: string
|
||||
agreementTitle: string
|
||||
isEnableExternalLinks: string
|
||||
externalLinks: string
|
||||
clearCacheEnabled: string
|
||||
noticeTitle: string
|
||||
streamCacheEnabled: string
|
||||
homeWelcomeContent: string
|
||||
sideDrawingEditModel: 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
|
||||
isBindWx: boolean
|
||||
consecutiveDays: number
|
||||
nickname: string
|
||||
}
|
||||
userBalance: UserBalance
|
||||
globalConfig: GlobalConfig
|
||||
}
|
||||
98
chat/src/store/modules/auth/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useChatStore } from '../chat'
|
||||
|
||||
import { fetchGetInfo } from '@/api'
|
||||
import { fetchGetBalanceQueryAPI } from '@/api/balance'
|
||||
import { fetchQueryConfigAPI } from '@/api/config'
|
||||
import type { ResData } from '@/api/types'
|
||||
import { store } from '@/store'
|
||||
import type { AuthState, GlobalConfig, UserBalance } from './helper'
|
||||
import { getToken, removeToken, setToken } from './helper'
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
updateUserBalance(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 as GlobalConfig
|
||||
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 = {}
|
||||
// message().success('登出账户成功!')
|
||||
const chatStore = useChatStore()
|
||||
chatStore.clearChat()
|
||||
window.location.reload()
|
||||
},
|
||||
|
||||
updatePasswordSuccess() {
|
||||
this.token = undefined
|
||||
removeToken()
|
||||
this.userInfo = {}
|
||||
this.userBalance = {}
|
||||
this.loginDialog = true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function useAuthStoreWithout() {
|
||||
return useAuthStore(store)
|
||||
}
|
||||
46
chat/src/store/modules/chat/helper.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'chatStorage'
|
||||
|
||||
export function defaultState(): Chat.ChatState {
|
||||
return {
|
||||
active: 0,
|
||||
usingContext: true,
|
||||
usingNetwork: false,
|
||||
usingDeepThinking: false,
|
||||
usingMcpTool: false,
|
||||
groupList: [],
|
||||
chatList: [],
|
||||
groupKeyWord: '',
|
||||
baseConfig: null,
|
||||
currentPlugin: undefined,
|
||||
pluginList: [],
|
||||
prompt: '',
|
||||
reasoningText: '',
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
396
chat/src/store/modules/chat/index.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import {
|
||||
fetchCreateGroupAPI,
|
||||
fetchDelAllGroupAPI,
|
||||
fetchDelGroupAPI,
|
||||
fetchQueryGroupAPI,
|
||||
fetchUpdateGroupAPI,
|
||||
} from '@/api/group'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getLocalState, setLocalState } from './helper'
|
||||
|
||||
import {
|
||||
fetchDelChatLogAPI,
|
||||
fetchDelChatLogByGroupIdAPI,
|
||||
fetchDeleteGroupChatsAfterIdAPI,
|
||||
fetchQueryChatLogListAPI,
|
||||
} from '@/api/chatLog'
|
||||
import { fetchModelBaseConfigAPI } from '@/api/models'
|
||||
import { fetchQueryPluginsAPI } from '@/api/plugin'
|
||||
import { useGlobalStoreWithOut } from '@/store'
|
||||
|
||||
const useGlobalStore = useGlobalStoreWithOut()
|
||||
|
||||
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
|
||||
const parsedConfig = config ? JSON.parse(config) : state.baseConfig
|
||||
|
||||
return parsedConfig
|
||||
},
|
||||
|
||||
activeGroupAppId: state => {
|
||||
const uuid = state.active
|
||||
if (!uuid) return null
|
||||
return state.groupList.find(item => item.uuid === uuid)?.appId
|
||||
},
|
||||
|
||||
activeGroupFileUrl: state => {
|
||||
const uuid = state.active
|
||||
if (!uuid) return null
|
||||
return state.groupList.find(item => item.uuid === uuid)?.fileUrl
|
||||
},
|
||||
|
||||
/* 当前选用模型的名称 */
|
||||
activeModel(state) {
|
||||
return this.activeConfig?.modelInfo?.model
|
||||
},
|
||||
|
||||
/* 当前选用模型的名称 */
|
||||
activeModelName(state) {
|
||||
return this.activeConfig?.modelInfo?.modelName
|
||||
},
|
||||
|
||||
/* 当前选用模型的名称 */
|
||||
activeModelAvatar(state) {
|
||||
return this.activeConfig?.modelInfo?.modelAvatar
|
||||
},
|
||||
|
||||
/* 当前选用模型的扣费类型 */
|
||||
activeModelDeductType(state) {
|
||||
return this.activeConfig?.modelInfo?.deductType
|
||||
},
|
||||
|
||||
/* 当前选用模型的模型类型 */
|
||||
activeModelKeyType(state) {
|
||||
return this.activeConfig?.modelInfo?.keyType
|
||||
},
|
||||
|
||||
/* 当前选用模型支持上传文件的格式 */
|
||||
activeModelFileUpload(state) {
|
||||
return this.activeConfig?.modelInfo?.isFileUpload
|
||||
},
|
||||
|
||||
/* 当前选用模型的调用价格 */
|
||||
activeModelPrice(state) {
|
||||
return this.activeConfig?.modelInfo?.deduct
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/* 查询插件列表 */
|
||||
async queryPlugins() {
|
||||
try {
|
||||
const res: any = await fetchQueryPluginsAPI()
|
||||
if (res.success && res.code === 200) {
|
||||
// 过滤掉不启用的插件并只保留需要的字段
|
||||
this.pluginList = res.data.rows
|
||||
.filter((plugin: any) => plugin.isEnabled === 1)
|
||||
.map((plugin: any) => ({
|
||||
pluginId: plugin.id,
|
||||
pluginName: plugin.name,
|
||||
description: plugin.description,
|
||||
pluginImg: plugin.pluginImg,
|
||||
parameters: plugin.parameters,
|
||||
deductType: plugin.deductType,
|
||||
drawingType: plugin.drawingType,
|
||||
modelType: plugin.modelType,
|
||||
}))
|
||||
} else {
|
||||
}
|
||||
} catch (error) {}
|
||||
},
|
||||
|
||||
/* 对话组过滤 */
|
||||
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, modelConfig?: any, params?: string) {
|
||||
try {
|
||||
const res: any = await fetchCreateGroupAPI({
|
||||
appId,
|
||||
modelConfig,
|
||||
params,
|
||||
})
|
||||
|
||||
this.active = res.data.id
|
||||
this.usingNetwork = false
|
||||
this.usingDeepThinking = false
|
||||
this.usingMcpTool = false
|
||||
this.recordState()
|
||||
|
||||
await this.queryMyGroup()
|
||||
|
||||
await this.setActiveGroup(res.data.id)
|
||||
} catch (error) {}
|
||||
},
|
||||
|
||||
/* 查询基础模型配置 */
|
||||
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,
|
||||
isFixedModel,
|
||||
isGpts,
|
||||
params,
|
||||
fileUrl,
|
||||
content,
|
||||
appModel,
|
||||
} = item
|
||||
return {
|
||||
uuid,
|
||||
title,
|
||||
isEdit: false,
|
||||
appId,
|
||||
config,
|
||||
isSticky,
|
||||
appLogo,
|
||||
createdAt,
|
||||
isFixedModel,
|
||||
isGpts,
|
||||
params,
|
||||
fileUrl,
|
||||
content,
|
||||
appModel,
|
||||
updatedAt: new Date(updatedAt).getTime(),
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
const isHasActive = this.groupList.some(
|
||||
(item: { uuid: any }) => Number(item.uuid) === Number(this.active)
|
||||
)
|
||||
if (!this.active || !isHasActive) {
|
||||
this.groupList.length && this.setActiveGroup(this.groupList[0].uuid)
|
||||
}
|
||||
// 如果 groupList 为空,新建一个对话组
|
||||
if (this.groupList.length === 0) {
|
||||
await this.addNewChatGroup()
|
||||
}
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 修改对话组信息 */
|
||||
async updateGroupInfo(params: {
|
||||
groupId: number
|
||||
title?: string
|
||||
isSticky?: boolean
|
||||
fileUrl?: string
|
||||
}) {
|
||||
await fetchUpdateGroupAPI(params)
|
||||
await this.queryMyGroup()
|
||||
},
|
||||
|
||||
/* 变更对话组 */
|
||||
// 设置当前激活的对话组
|
||||
async setActiveGroup(uuid: number) {
|
||||
useGlobalStore.updateShowAppListComponent(false)
|
||||
// useGlobalStore.updateImagePreviewer(false)
|
||||
// this.chatList = [];
|
||||
this.active = uuid
|
||||
|
||||
this.groupList.forEach(item => (item.isEdit = false))
|
||||
await this.queryActiveChatLogList()
|
||||
if (this.active) {
|
||||
await this.queryActiveChatLogList()
|
||||
} else {
|
||||
this.chatList = []
|
||||
}
|
||||
this.active = uuid
|
||||
// 记录当前状态
|
||||
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;
|
||||
// this.recordState();
|
||||
// },
|
||||
|
||||
/* 查询当前对话组的聊天记录 */
|
||||
/* 查询当前对话组的聊天记录 */
|
||||
async queryActiveChatLogList() {
|
||||
// 如果没有激活的对话组,或者 groupId 为 0,则不进行查询
|
||||
if (!this.active || Number(this.active) === 0) {
|
||||
this.chatList = [] // 确保没有数据时清空 chatList
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 API 查询聊天记录
|
||||
const res: any = await fetchQueryChatLogListAPI({
|
||||
groupId: this.active,
|
||||
})
|
||||
|
||||
// 检查响应数据并更新 chatList
|
||||
if (res && res.data) {
|
||||
this.chatList = res.data
|
||||
} else {
|
||||
this.chatList = [] // 如果没有数据,确保 chatList 为空数组
|
||||
}
|
||||
} catch (error) {
|
||||
// 捕获错误并处理
|
||||
|
||||
this.chatList = [] // 出错时清空 chatList
|
||||
} finally {
|
||||
// 无论成功还是失败,都调用 recordState
|
||||
|
||||
this.recordState()
|
||||
}
|
||||
},
|
||||
|
||||
/* 添加一条虚拟的对话记录 */
|
||||
addGroupChat(data: Chat.Chat) {
|
||||
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) {
|
||||
if (!chatId) return
|
||||
await fetchDelChatLogAPI({ id: chatId })
|
||||
await this.queryActiveChatLogList()
|
||||
},
|
||||
|
||||
/* 删除一条对话记录 */
|
||||
async deleteChatsAfterId(chatId: number | undefined) {
|
||||
if (!chatId) return
|
||||
await fetchDeleteGroupChatsAfterIdAPI({ id: chatId })
|
||||
await this.queryActiveChatLogList()
|
||||
},
|
||||
|
||||
/* 设置使用上下文 */
|
||||
setUsingContext(context: boolean) {
|
||||
this.usingContext = context
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 设置使用联网 */
|
||||
setUsingNetwork(context: boolean) {
|
||||
this.usingNetwork = context
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 设置使用深度思考 */
|
||||
setUsingDeepThinking(context: boolean) {
|
||||
this.usingDeepThinking = context
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
/* 设置使用 MCP 工具 */
|
||||
setUsingMcpTool(context: boolean) {
|
||||
this.usingMcpTool = context
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
setUsingPlugin(plugin: any) {
|
||||
// Set the current plugin to the new plugin if provided, else clear it
|
||||
this.currentPlugin = plugin || undefined
|
||||
this.recordState() // Record the state change
|
||||
},
|
||||
|
||||
async setPrompt(prompt: string) {
|
||||
this.prompt = prompt
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
setStreamIn(isStreamIn: boolean) {
|
||||
this.isStreamIn = isStreamIn
|
||||
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()
|
||||
},
|
||||
},
|
||||
})
|
||||
88
chat/src/store/modules/global/helper.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
import { UserState } from '../users/helper'
|
||||
|
||||
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 {
|
||||
loading: boolean
|
||||
showLoginDialog: boolean
|
||||
goodsDialog: boolean
|
||||
fingerprint: number
|
||||
noticeDialog: boolean
|
||||
bindWxDialog: boolean
|
||||
signInDialog: boolean
|
||||
appDialog: boolean
|
||||
identityDialog: boolean
|
||||
phoneIdentityDialog: boolean
|
||||
userAgreementDialog: boolean
|
||||
BadWordsDialog: boolean
|
||||
htmlDialog: boolean
|
||||
pythonDialog: boolean
|
||||
pythonContent: string
|
||||
settingsDialog: boolean
|
||||
mobileSettingsDialog: boolean
|
||||
settingsActiveTab: number
|
||||
mobileInitialTab?: string
|
||||
isChatIn: boolean
|
||||
isCacheEnabled: boolean
|
||||
orderInfo: OrderInfo
|
||||
model: number
|
||||
iframeUrl: string
|
||||
clipboardText: string
|
||||
htmlContent: string
|
||||
contentType: 'html' | 'mermaid' | 'markmap' | ''
|
||||
textContent: string
|
||||
full_json: string
|
||||
externalLinkDialog: boolean
|
||||
showAppListComponent: boolean
|
||||
showBadWordsDialog: boolean
|
||||
showHtmlPreviewer: boolean
|
||||
showTextEditor: boolean
|
||||
showImagePreviewer: boolean
|
||||
showWorkflowPreviewer: boolean
|
||||
showMarkdownPreviewer: boolean
|
||||
isMarkdownPreviewerVisible: boolean
|
||||
previewImageUrls: string[]
|
||||
initialImageIndex: number
|
||||
currentExternalLink: string | null
|
||||
mjImageData: any
|
||||
workflowContent: string[]
|
||||
markdownContent: string
|
||||
}
|
||||
|
||||
export function defaultSetting(): UserState {
|
||||
return {
|
||||
userInfo: {
|
||||
avatar: '',
|
||||
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)
|
||||
}
|
||||
291
chat/src/store/modules/global/index.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { store } from '@/store'
|
||||
import { ss } from '@/utils/storage'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useChatStore } from '../chat'
|
||||
import type { GlobalState } from './helper'
|
||||
|
||||
// 定义对话框的索引常量,方便后续使用
|
||||
export const DIALOG_TABS = {
|
||||
ACCOUNT: 0, // 账户管理
|
||||
MEMBER: 1, // 会员中心
|
||||
NOTICE: 2, // 网站公告
|
||||
AGREEMENT: 3, // 用户协议
|
||||
}
|
||||
|
||||
export const useGlobalStore = defineStore('global-store', {
|
||||
state: (): GlobalState => ({
|
||||
loading: false,
|
||||
showAppListComponent: false,
|
||||
settingsDialog: false,
|
||||
showLoginDialog: false,
|
||||
showBadWordsDialog: false,
|
||||
showHtmlPreviewer: false,
|
||||
showTextEditor: false,
|
||||
showImagePreviewer: false,
|
||||
showWorkflowPreviewer: false,
|
||||
showMarkdownPreviewer: false,
|
||||
previewImageUrls: [],
|
||||
initialImageIndex: 0,
|
||||
pythonDialog: false,
|
||||
htmlDialog: false,
|
||||
isChatIn: false,
|
||||
settingsActiveTab: 0,
|
||||
htmlContent: '',
|
||||
contentType: '',
|
||||
textContent: '',
|
||||
pythonContent: '',
|
||||
full_json: '',
|
||||
externalLinkDialog: false,
|
||||
currentExternalLink: null,
|
||||
mobileSettingsDialog: false,
|
||||
goodsDialog: false,
|
||||
fingerprint: 0,
|
||||
noticeDialog: false,
|
||||
bindWxDialog: false,
|
||||
signInDialog: false,
|
||||
appDialog: false,
|
||||
identityDialog: false,
|
||||
phoneIdentityDialog: false,
|
||||
userAgreementDialog: false,
|
||||
BadWordsDialog: false,
|
||||
isCacheEnabled: false,
|
||||
orderInfo: {
|
||||
pkgInfo: {
|
||||
id: 0,
|
||||
des: '',
|
||||
name: '',
|
||||
price: '',
|
||||
model3Count: 0,
|
||||
model4Count: 0,
|
||||
drawMjCount: 0,
|
||||
coverImg: '',
|
||||
days: 0,
|
||||
},
|
||||
},
|
||||
model: 0,
|
||||
iframeUrl: '',
|
||||
clipboardText: '',
|
||||
mjImageData: {},
|
||||
mobileInitialTab: undefined,
|
||||
workflowContent: [],
|
||||
markdownContent: '',
|
||||
isMarkdownPreviewerVisible: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
updateClipboardText(text: string) {
|
||||
this.clipboardText = text
|
||||
},
|
||||
|
||||
updateTextContent(text: string) {
|
||||
this.textContent = text
|
||||
},
|
||||
|
||||
updateFullJson(json: string) {
|
||||
this.full_json = json
|
||||
},
|
||||
|
||||
updateFingerprint(str: number) {
|
||||
let id = str
|
||||
/* 超过mysql最大值进行截取 */
|
||||
if (id > 2147483647) {
|
||||
id = Number(id.toString().slice(-9))
|
||||
id = Number(String(Number(id)))
|
||||
}
|
||||
ss.set('fingerprint', id)
|
||||
this.fingerprint = id
|
||||
},
|
||||
|
||||
updateIframeUrl(iframeUrl: string) {
|
||||
this.iframeUrl = iframeUrl
|
||||
},
|
||||
|
||||
updateUserAgreementDialog(userAgreementDialog: boolean) {
|
||||
this.userAgreementDialog = userAgreementDialog
|
||||
},
|
||||
|
||||
UpdateBadWordsDialog(BadWordsDialog: boolean) {
|
||||
this.BadWordsDialog = BadWordsDialog
|
||||
},
|
||||
|
||||
updateHtmlContent(
|
||||
htmlContent: string,
|
||||
contentType: 'html' | 'mermaid' | 'markmap' | '' = 'html'
|
||||
) {
|
||||
this.htmlContent = htmlContent
|
||||
this.contentType = contentType
|
||||
},
|
||||
|
||||
updateHtmlPreviewer(visible: boolean) {
|
||||
this.showHtmlPreviewer = visible
|
||||
},
|
||||
|
||||
updateTextEditor(visible: boolean) {
|
||||
this.showTextEditor = visible
|
||||
},
|
||||
|
||||
updateImagePreviewer(
|
||||
visible: boolean,
|
||||
imageUrls: string[] = [],
|
||||
initialIndex: number = 0,
|
||||
mjData?: any
|
||||
) {
|
||||
this.showImagePreviewer = visible
|
||||
if (visible) {
|
||||
this.previewImageUrls = imageUrls
|
||||
this.initialImageIndex = initialIndex
|
||||
this.mjImageData = mjData || {}
|
||||
// 当图片预览器启用时,自动清空正在使用的插件
|
||||
const chatStore = useChatStore()
|
||||
chatStore.setUsingPlugin(null)
|
||||
}
|
||||
},
|
||||
|
||||
updateIsChatIn(isChatIn: boolean) {
|
||||
this.isChatIn = isChatIn
|
||||
},
|
||||
|
||||
updateGoodsDialog(goodsDialog: boolean) {
|
||||
this.goodsDialog = goodsDialog
|
||||
},
|
||||
|
||||
updateBindwxDialog(bindWxDialog: boolean) {
|
||||
this.bindWxDialog = bindWxDialog
|
||||
},
|
||||
|
||||
updateSignInDialog(signInDialog: boolean) {
|
||||
this.signInDialog = signInDialog
|
||||
},
|
||||
|
||||
updateNoticeDialog(noticeDialog: boolean) {
|
||||
this.noticeDialog = noticeDialog
|
||||
},
|
||||
|
||||
updateAppDialog(appDialog: boolean) {
|
||||
this.appDialog = appDialog
|
||||
},
|
||||
|
||||
updateIdentityDialog(identityDialog: boolean) {
|
||||
this.identityDialog = identityDialog
|
||||
},
|
||||
|
||||
updatePhoneDialog(phoneIdentityDialog: boolean) {
|
||||
this.phoneIdentityDialog = phoneIdentityDialog
|
||||
},
|
||||
|
||||
updateHtmlDialog(htmlDialog: boolean) {
|
||||
this.htmlDialog = htmlDialog
|
||||
},
|
||||
|
||||
updateModel(model: number) {
|
||||
ss.set('model', model)
|
||||
this.model = model
|
||||
},
|
||||
|
||||
updateOrderInfo(info: any) {
|
||||
// Add appropriate type
|
||||
this.orderInfo = info
|
||||
},
|
||||
|
||||
updatePythonDialog(pythonDialog: boolean) {
|
||||
this.pythonDialog = pythonDialog
|
||||
},
|
||||
|
||||
updatePythonContent(content: string) {
|
||||
console.log('updatePythonContent', content)
|
||||
this.pythonContent = content
|
||||
},
|
||||
|
||||
updateExternalLinkDialog(visible: boolean, url: string | null = null) {
|
||||
this.externalLinkDialog = visible
|
||||
this.currentExternalLink = url
|
||||
},
|
||||
|
||||
updateSettingsDialog(settingsDialog: boolean, activeTab?: number) {
|
||||
this.settingsDialog = settingsDialog
|
||||
if (settingsDialog && activeTab !== undefined) {
|
||||
this.settingsActiveTab = activeTab
|
||||
}
|
||||
},
|
||||
|
||||
updateMobileSettingsDialog(mobileSettingsDialog: boolean, activeTab?: number | string) {
|
||||
this.mobileSettingsDialog = mobileSettingsDialog
|
||||
|
||||
if (activeTab !== undefined) {
|
||||
// 如果是数字索引,转换为对应的tabId
|
||||
if (typeof activeTab === 'number') {
|
||||
const tabIds = ['account', 'member', 'notice', 'agreement']
|
||||
this.mobileInitialTab = tabIds[activeTab] || undefined
|
||||
} else {
|
||||
// 如果直接传入了tabId字符串
|
||||
this.mobileInitialTab = activeTab
|
||||
}
|
||||
} else {
|
||||
this.mobileInitialTab = undefined
|
||||
}
|
||||
},
|
||||
|
||||
updateShowAppListComponent(showAppListComponent: boolean) {
|
||||
this.showAppListComponent = showAppListComponent
|
||||
},
|
||||
|
||||
setCurrentExternalLink(link: string | null) {
|
||||
this.currentExternalLink = link
|
||||
if (link) {
|
||||
this.externalLinkDialog = true
|
||||
}
|
||||
},
|
||||
|
||||
updateSettingsActiveTab(tab: number) {
|
||||
this.settingsActiveTab = tab
|
||||
},
|
||||
|
||||
updateWorkflowPreviewer(visible: boolean) {
|
||||
this.showWorkflowPreviewer = visible
|
||||
if (!visible) {
|
||||
this.workflowContent = []
|
||||
}
|
||||
},
|
||||
|
||||
addWorkflowContent(content: string) {
|
||||
this.workflowContent.push(content)
|
||||
},
|
||||
|
||||
clearWorkflowContent() {
|
||||
this.workflowContent = []
|
||||
},
|
||||
|
||||
updateWorkflowContentAt(index: number, content: string) {
|
||||
if (index >= 0 && index < this.workflowContent.length) {
|
||||
this.workflowContent[index] = content
|
||||
}
|
||||
},
|
||||
|
||||
updateWorkflowContentLast(newContent: string) {
|
||||
const currentWorkflow = this.workflowContent
|
||||
if (currentWorkflow.length > 0) {
|
||||
// 获取最后一项的索引
|
||||
const lastIndex = currentWorkflow.length - 1
|
||||
// 将新内容追加到最后一项
|
||||
this.workflowContent[lastIndex] += newContent
|
||||
} else {
|
||||
// 如果还没有内容,则创建新项
|
||||
this.workflowContent.push(newContent)
|
||||
}
|
||||
},
|
||||
|
||||
updateMarkdownPreviewer(visible: boolean, markdownContent?: string) {
|
||||
this.isMarkdownPreviewerVisible = visible
|
||||
if (markdownContent) {
|
||||
this.markdownContent = markdownContent
|
||||
}
|
||||
if (!visible) {
|
||||
this.markdownContent = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function useGlobalStoreWithOut() {
|
||||
return useGlobalStore(store)
|
||||
}
|
||||
8
chat/src/store/modules/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './app'
|
||||
export * from './chat'
|
||||
// export * from './user'
|
||||
export * from './appStore'
|
||||
export * from './auth'
|
||||
export * from './global'
|
||||
export * from './prompt'
|
||||
export * from './settings'
|
||||
19
chat/src/store/modules/prompt/helper.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'promptStore'
|
||||
|
||||
export type PromptList = []
|
||||
|
||||
export interface PromptStore {
|
||||
promptList: PromptList
|
||||
}
|
||||
|
||||
export function getLocalPromptList(): PromptStore {
|
||||
const storage = ss.get(LOCAL_NAME)
|
||||
const promptStore: PromptStore | undefined = storage
|
||||
return promptStore ?? { promptList: [] }
|
||||
}
|
||||
|
||||
export function setLocalPromptList(promptStore: PromptStore): void {
|
||||
ss.set(LOCAL_NAME, promptStore)
|
||||
}
|
||||
17
chat/src/store/modules/prompt/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { PromptStore } from './helper'
|
||||
import { getLocalPromptList, setLocalPromptList } from './helper'
|
||||
|
||||
export const usePromptStore = defineStore('prompt-store', {
|
||||
state: (): PromptStore => getLocalPromptList(),
|
||||
|
||||
actions: {
|
||||
updatePromptList(promptList: []) {
|
||||
this.$patch({ promptList })
|
||||
setLocalPromptList({ promptList })
|
||||
},
|
||||
getPromptList() {
|
||||
return this.$state
|
||||
},
|
||||
},
|
||||
})
|
||||
26
chat/src/store/modules/settings/helper.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'settingsStorage'
|
||||
|
||||
export interface SettingsState {
|
||||
systemMessage: string
|
||||
}
|
||||
|
||||
export function defaultSetting(): SettingsState {
|
||||
return {
|
||||
systemMessage: '',
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalState(): SettingsState {
|
||||
const localSetting: SettingsState | undefined = ss.get(LOCAL_NAME)
|
||||
return { ...defaultSetting(), ...localSetting }
|
||||
}
|
||||
|
||||
export function setLocalState(setting: SettingsState): void {
|
||||
ss.set(LOCAL_NAME, setting)
|
||||
}
|
||||
|
||||
export function removeLocalState() {
|
||||
ss.remove(LOCAL_NAME)
|
||||
}
|
||||
22
chat/src/store/modules/settings/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { SettingsState } from './helper'
|
||||
import { defaultSetting, getLocalState, removeLocalState, setLocalState } from './helper'
|
||||
|
||||
export const useSettingStore = defineStore('setting-store', {
|
||||
state: (): SettingsState => getLocalState(),
|
||||
actions: {
|
||||
updateSetting(settings: Partial<SettingsState>) {
|
||||
this.$state = { ...this.$state, ...settings }
|
||||
this.recordState()
|
||||
},
|
||||
|
||||
resetSetting() {
|
||||
this.$state = defaultSetting()
|
||||
removeLocalState()
|
||||
},
|
||||
|
||||
recordState() {
|
||||
setLocalState(this.$state)
|
||||
},
|
||||
},
|
||||
})
|
||||
30
chat/src/store/modules/users/helper.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'userStorage'
|
||||
|
||||
export interface UserInfo {
|
||||
avatar: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
userInfo: UserInfo
|
||||
}
|
||||
|
||||
export function defaultSetting(): UserState {
|
||||
return {
|
||||
userInfo: {
|
||||
avatar: '',
|
||||
name: 'Ai Web',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||