This commit is contained in:
Sun
2023-11-08 21:53:07 +08:00
commit 211c3071dc
245 changed files with 39293 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
<script setup lang='ts'>
import { NImage } from 'naive-ui'
import { ref } from 'vue'
defineProps<{
src: string
}>()
const emit = defineEmits<{
(event: 'click'): void
(event: 'refresh'): void
}>()
const randCode = ref<string>('0')
function handleClick() {
randCode.value = String(rand(100, 99999))
emit('click')
}
function rand(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min
}
defineExpose({
// 刷新验证码
refresh() {
handleClick()
},
})
</script>
<template>
<!-- <div> -->
<NImage
:src="`${src}?${randCode}`"
:preview-disabled="true"
@click="handleClick"
/>
<!-- </div> -->
</template>
@@ -0,0 +1,20 @@
<script setup lang='ts'>
interface Emit {
(e: 'click'): void
}
const emit = defineEmits<Emit>()
function handleClick() {
emit('click')
}
</script>
<template>
<button
class="flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100 dark:hover:bg-[#414755]"
@click="handleClick"
>
<slot />
</button>
</template>
@@ -0,0 +1,46 @@
<script setup lang='ts'>
import { computed } from 'vue'
import type { PopoverPlacement } from 'naive-ui'
import { NTooltip } from 'naive-ui'
import Button from './Button.vue'
interface Props {
tooltip?: string
placement?: PopoverPlacement
}
interface Emit {
(e: 'click'): void
}
const props = withDefaults(defineProps<Props>(), {
tooltip: '',
placement: 'bottom',
})
const emit = defineEmits<Emit>()
const showTooltip = computed(() => Boolean(props.tooltip))
function handleClick() {
emit('click')
}
</script>
<template>
<div v-if="showTooltip">
<NTooltip :placement="placement" trigger="hover">
<template #trigger>
<Button @click="handleClick">
<slot />
</Button>
</template>
{{ tooltip }}
</NTooltip>
</div>
<div v-else>
<Button @click="handleClick">
<slot />
</Button>
</div>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { NAvatar, NImage } from 'naive-ui'
import { computed, withDefaults } from 'vue'
import { SvgIcon } from '@/components/common'
interface Prop {
itemIcon?: Panel.ItemIcon | null
size?: number // 默认70
}
const props = withDefaults(defineProps<Prop>(), { size: 70 })
const defaultStyle = { width: `${props.size}px`, height: `${props.size}px` }
const iconExt = computed(() => {
return props.itemIcon?.src?.split('.').pop()
})
</script>
<template>
<div class="overflow-hidden rounded-2xl" :style="defaultStyle">
<slot>
<div v-if="itemIcon">
<div v-if="itemIcon?.itemType === 1">
<NAvatar :size="props.size" :style="{ backgroundColor: itemIcon?.bgColor }">
{{ itemIcon.text }}
</NAvatar>
</div>
<div v-else-if="itemIcon?.itemType === 2">
<div v-if="iconExt === 'svg'" :style="defaultStyle" class="flex justify-center items-center">
<img :src="itemIcon?.src" class="w-[35px] h-[35px]">
<!-- <object :data="itemIcon?.src" type="image/svg+xml" class="w-[35px] h-[35px]" style="fill: rgb(255, 255, 255) !important;" /> -->
</div>
<NImage v-else :style="defaultStyle" :src="itemIcon?.src" preview-disabled />
</div>
<div v-else-if="itemIcon?.itemType === 3">
<NAvatar :size="props.size" :style="{ backgroundColor: itemIcon?.bgColor }">
<SvgIcon style="font-size: 35px;" :icon="itemIcon.text" />
</NAvatar>
</div>
</div>
<div v-else>
<NAvatar :size="props.size" />
</div>
</slot>
</div>
</template>
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import {
NDialogProvider,
NLoadingBarProvider,
NMessageProvider,
NNotificationProvider,
useDialog,
useLoadingBar,
useMessage,
useNotification,
} from 'naive-ui'
function registerNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$dialog = useDialog()
window.$message = useMessage()
window.$notification = useNotification()
}
const NaiveProviderContent = defineComponent({
name: 'NaiveProviderContent',
setup() {
registerNaiveTools()
},
render() {
return h('div')
},
})
</script>
<template>
<NLoadingBarProvider>
<NDialogProvider>
<NNotificationProvider>
<NMessageProvider>
<slot />
<NaiveProviderContent />
</NMessageProvider>
</NNotificationProvider>
</NDialogProvider>
</NLoadingBarProvider>
</template>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { computed, useAttrs } from 'vue'
import { NModal } from 'naive-ui'
const props = defineProps<{
title?: string
show: boolean
size?: 'medium' | 'small' | 'large' | 'huge' | undefined
}>()
const emit = defineEmits<Emit>()
interface Emit {
(e: 'update:show', show: boolean): void
// (e: 'done', item: Panel.Info): void// 创建完成
}
const attrs = useAttrs()
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || '',
}))
// 更新值父组件传来的值
const showModal = computed({
get: () => props.show,
set: (show: boolean) => {
emit('update:show', show)
},
})
</script>
<template>
<NModal v-model:show="showModal" preset="card" :size="size" v-bind="bindAttrs" style="border-radius: 1rem;width: 600px;" :title="title">
<template #cover>
<slot name="cover" />
</template>
<template #header>
<slot name="header" />
</template>
<template #eader-extra>
<slot name="header-extra" />
</template>
<template #footer>
<slot name="footer" />
</template>
<template #action>
<slot name="action" />
</template>
<slot />
</NModal>
</template>
+82
View File
@@ -0,0 +1,82 @@
<script setup lang='ts'>
import { onMounted, ref } from 'vue'
import { NSpin } from 'naive-ui'
import { fetchChatConfig } from '@/api'
import { getAboutDescription as getAboutDescriptionApi } from '@/api/openness'
interface ConfigState {
timeoutMs?: number
reverseProxy?: string
apiModel?: string
socksProxy?: string
httpsProxy?: string
usage?: string
}
const loading = ref(false)
const config = ref<ConfigState>()
const content = ref<string>()
async function fetchConfig() {
try {
loading.value = true
const { data } = await fetchChatConfig<ConfigState>()
config.value = data
}
finally {
loading.value = false
}
}
onMounted(() => {
fetchConfig()
getAboutDescription()
})
async function getAboutDescription() {
const { data } = await getAboutDescriptionApi<string>()
content.value = data
}
</script>
<template>
<NSpin :show="loading">
<div class="p-4 space-y-4">
<h2 class="text-xl font-bold">
{{ $t('common.appName') }}
</h2>
<div>
<span v-html="content" />
</div>
<!-- <div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
<p>
此项目开源于
<a
class="text-blue-600 dark:text-blue-500"
href="https://github.com/Chanzhaoyu/chatgpt-web"
target="_blank"
>
GitHub
</a>
免费且基于 MIT 协议没有任何形式的付费行为
</p>
<p>
如果你觉得此项目对你有帮助请在 GitHub 帮我点个 Star 或者给予一点赞助谢谢
</p>
</div> -->
<!-- <p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p>
<p v-if="isChatGPTAPI">
{{ $t("setting.monthlyUsage") }}{{ config?.usage ?? '-' }}
</p>
<p v-if="!isChatGPTAPI">
{{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }}
</p>
<p>{{ $t("setting.timeout") }}{{ config?.timeoutMs ?? '-' }}</p>
<p>{{ $t("setting.socks") }}{{ config?.socksProxy ?? '-' }}</p>
<p>{{ $t("setting.httpsProxy") }}{{ config?.httpsProxy ?? '-' }}</p> -->
</div>
</NSpin>
</template>
+225
View File
@@ -0,0 +1,225 @@
<!-- eslint-disable eslint-comments/no-unlimited-disable
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
import type { Language, Theme } from '@/store/modules/app/helper'
import { SvgIcon } from '@/components/common'
import { useAppStore, useUserStore } from '@/store'
import type { UserInfo } from '@/store/modules/user/helper'
import { getCurrentDate } from '@/utils/functions'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
const appStore = useAppStore()
const userStore = useUserStore()
const { isMobile } = useBasicLayout()
const ms = useMessage()
const theme = computed(() => appStore.theme)
const userInfo = computed(() => userStore.userInfo)
const avatar = ref(userInfo.value.headImage ?? '')
const name = ref(userInfo.value.name ?? '')
// const description = ref(userInfo.value.description ?? '')
const language = computed({
get() {
return appStore.language
},
set(value: Language) {
appStore.setLanguage(value)
},
})
const themeOptions: { label: string; key: Theme; icon: string }[] = [
{
label: 'Auto',
key: 'auto',
icon: 'ri:contrast-line',
},
{
label: 'Light',
key: 'light',
icon: 'ri:sun-foggy-line',
},
{
label: 'Dark',
key: 'dark',
icon: 'ri:moon-foggy-line',
},
]
const languageOptions: { label: string; key: Language; value: Language }[] = [
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
{ label: 'English', key: 'en-US', value: 'en-US' },
{ label: '한국어', key: 'ko-KR', value: 'ko-KR' },
]
function updateUserInfo(options: Partial<UserInfo>) {
// userStore.updateUserInfo(options)
ms.success(t('common.success'))
}
function handleReset() {
userStore.resetUserInfo()
ms.success(t('common.success'))
window.location.reload()
}
function exportData(): void {
const date = getCurrentDate()
const data: string = localStorage.getItem('chatStorage') || '{}'
const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
const blob: Blob = new Blob([jsonString], { type: 'application/json' })
const url: string = URL.createObjectURL(blob)
const link: HTMLAnchorElement = document.createElement('a')
link.href = url
link.download = `chat-store_${date}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function importData(event: Event): void {
const target = event.target as HTMLInputElement
if (!target || !target.files)
return
const file: File = target.files[0]
if (!file)
return
const reader: FileReader = new FileReader()
reader.onload = () => {
try {
const data = JSON.parse(reader.result as string)
localStorage.setItem('chatStorage', JSON.stringify(data))
ms.success(t('common.success'))
location.reload()
}
catch (error) {
ms.error(t('common.invalidFileFormat'))
}
}
reader.readAsText(file)
}
function clearData(): void {
localStorage.removeItem('chatStorage')
location.reload()
}
function handleImportButtonClick(): void {
const fileInput = document.getElementById('fileInput') as HTMLElement
if (fileInput)
fileInput.click()
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
<div class="flex-1">
<NInput v-model:value="avatar" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ headImage })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
<div class="w-[200px]">
<NInput v-model:value="name" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
<div class="flex-1">
<NInput v-model:value="description" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
{{ $t('common.save') }}
</NButton>
</div>
<div
class="flex items-center space-x-4"
:class="isMobile && 'items-start'"
>
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NButton size="small" @click="exportData">
<template #icon>
<SvgIcon icon="ri:download-2-fill" />
</template>
{{ $t('common.export') }}
</NButton>
<input id="fileInput" type="file" style="display:none" @change="importData">
<NButton size="small" @click="handleImportButtonClick">
<template #icon>
<SvgIcon icon="ri:upload-2-fill" />
</template>
{{ $t('common.import') }}
</NButton>
<NPopconfirm placement="bottom" @positive-click="clearData">
<template #trigger>
<NButton size="small">
<template #icon>
<SvgIcon icon="ri:close-circle-line" />
</template>
{{ $t('common.clear') }}
</NButton>
</template>
{{ $t('chat.clearHistoryConfirm') }}
</NPopconfirm>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
<div class="flex flex-wrap items-center gap-4">
<template v-for="item of themeOptions" :key="item.key">
<NButton
size="small"
:type="item.key === theme ? 'primary' : undefined"
@click="appStore.setTheme(item.key)"
>
<template #icon>
<SvgIcon :icon="item.icon" />
</template>
</NButton>
</template>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NSelect
style="width: 140px"
:value="language"
:options="languageOptions"
@update-value="value => appStore.setLanguage(value)"
/>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template> -->
@@ -0,0 +1,62 @@
<script setup lang='ts'>
import { onMounted, ref } from 'vue'
import { NButton, NInput, NInputGroup, NSpin } from 'naive-ui'
import { useClipboard } from '@vueuse/core'
import { getReferralCode as getReferralCodeApi } from '@/api/system/user'
// import { copyText } from '@/utils/format'
const loading = ref(false)
const referralCode = ref<string>('')
const url = ref<string>('')
const copyButtonText = ref<string>('')
async function getReferralCode() {
try {
loading.value = true
const { data } = await getReferralCodeApi<User.GetReferralCodeResponse>()
referralCode.value = data.referralCode
url.value = `${getCurrentDomain()}/#/register?referralCode=${referralCode.value}`
}
finally {
loading.value = false
}
}
function getCurrentDomain() {
return `${location.protocol}//${location.hostname}${location.port === '' ? '' : (`:${location.port}`)}`
}
function handleCopy() {
const { copy } = useClipboard()
// copyText({ text: urlCopy })
copy(url.value)
copyButtonText.value = '复制完成!'
setInterval(() => {
copyButtonText.value = '复制链接'
}, 3000)
}
onMounted(() => {
getReferralCode()
copyButtonText.value = '复制链接'
})
</script>
<template>
<NSpin :show="loading">
<div class="p-4 space-y-4">
<h2 class="text-xl ">
您的专属推荐链接
</h2>
<div>
<NInputGroup>
<NInput v-model:value="url" readonly type="text" />
<NButton type="primary" ghost @click="handleCopy">
{{ copyButtonText }}
</NButton>
</NInputGroup>
</div>
</div>
</NSpin>
</template>
+227
View File
@@ -0,0 +1,227 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import type { UploadFileInfo } from 'naive-ui'
import { NAvatar, NButton, NInput, NUpload, useDialog, useMessage } from 'naive-ui'
// import type { Language, Theme } from '@/store/modules/app/helper'
import type { Theme } from '@/store/modules/app/helper'
import { SvgIcon } from '@/components/common'
import { useAppStore, useAuthStore, useUserStore } from '@/store'
import type { UserInfo } from '@/store/modules/user/helper'
import { t } from '@/locales'
import { UserUpdateInfo, logout } from '@/api'
import { router } from '@/router'
// import defaultAvatar from '@/assets/userDefaultAvatar.png'
const appStore = useAppStore()
const userStore = useUserStore()
const authStore = useAuthStore()
const ms = useMessage()
const dialog = useDialog()
const theme = computed(() => appStore.theme)
const userInfo = computed(() => userStore.userInfo)
const avatar = ref(userInfo.value.headImage ?? '')
const name = ref(userInfo.value.name ?? '')
// const description = ref(userInfo.value.description ?? '')
// const language = computed({
// get() {
// return appStore.language
// },
// set(value: Language) {
// appStore.setLanguage(value)
// },
// })
const themeOptions: { label: string; key: Theme; icon: string }[] = [
{
label: 'Auto',
key: 'auto',
icon: 'ri:contrast-line',
},
{
label: 'Light',
key: 'light',
icon: 'ri:sun-foggy-line',
},
{
label: 'Dark',
key: 'dark',
icon: 'ri:moon-foggy-line',
},
]
// const languageOptions: { label: string; key: Language; value: Language }[] = [
// { label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
// { label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
// { label: 'English', key: 'en-US', value: 'en-US' },
// { label: '한국어', key: 'ko-KR', value: 'ko-KR' },
// ]
function updateUserInfo(options: Partial<UserInfo>) {
userStore.updateUserInfo({
headImage: userInfo.value.headImage,
name: options.name as string,
})
UserUpdateInfo(userInfo.value.headImage as string, options.name as string)
ms.success(t('common.success'))
}
// function uploadHeadImage(options: Partial<UserInfo>) {
// }
function onLogoutClick() {
dialog.warning({
title: '警告',
content: '你确定要退出登录',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
logoutApi()
},
})
}
async function logoutApi() {
await logout()
userStore.resetUserInfo()
authStore.removeToken()
ms.success('您已经安全退出,期待与你再次相见!')
router.push({ path: '/login' })
}
const handleFinish = ({
file,
event,
}: {
file: UploadFileInfo
event?: ProgressEvent
}) => {
const res = JSON.parse((event?.target as XMLHttpRequest).response)
// {
// "code": 0,
// "data": {
// "imageUrl": "/uploads/2023/5/12/c94306cfdf37fe7844753cd98fd57aaf.jpg"
// },
// "msg": "OK"
// }
const imageUrl = res.data.imageUrl
userStore.updateUserInfo({
headImage: imageUrl,
name: userInfo.value.name,
})
avatar.value = imageUrl
UserUpdateInfo(imageUrl, userInfo.value.name || '')
return file
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">账号/邮箱</span>
<div class="w-[200px]">
<NInput v-model:value="userInfo.username" :disabled="true" readonly placeholder="" />
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">密码</span>
<div class="w-[200px]">
<NButton @click="router.push({ path: '/resetPassword', query: { u: userInfo.username } })">
重置密码
</NButton>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatar') }}</span>
<div class="w-[50px]">
<NAvatar
round
:size="48"
:src="avatar ? avatar : ''"
/>
</div>
<NUpload
action="/api/file/uploadImg"
name="imgfile"
:headers="{
token: authStore.token as string,
}"
@finish="handleFinish"
>
<NButton size="tiny" text type="primary">
上传文件
</NButton>
</NUpload>
<!-- <NButton size="tiny" text type="primary" @click="uploadHeadImage({ avatar })">
选择文件
</NButton> -->
</div>
<!-- <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
<div class="flex-1">
<NInput v-model:value="avatar" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
{{ $t('common.save') }}
</NButton>
</div> -->
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
<div class="w-[200px]">
<NInput v-model:value="name" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
<div class="flex flex-wrap items-center gap-4">
<template v-for="item of themeOptions" :key="item.key">
<NButton
size="small"
:type="item.key === theme ? 'primary' : undefined"
@click="appStore.setTheme(item.key)"
>
<template #icon>
<SvgIcon :icon="item.icon" />
</template>
</NButton>
</template>
</div>
</div>
<!-- <div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NSelect
style="width: 140px"
:value="language"
:options="languageOptions"
@update-value="value => appStore.setLanguage(value)"
/>
</div>
</div> -->
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]" />
<NButton size="small" type="error" strong secondary @click="onLogoutClick">
<template #icon>
<SvgIcon icon="oi:account-logout" />
</template>
安全退出账号
</NButton>
</div>
</div>
</div>
</template>
+80
View File
@@ -0,0 +1,80 @@
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NModal, NTabPane, NTabs } from 'naive-ui'
import UserInfo from './UserInfo.vue'
// import Advanced from './Advanced.vue'
import About from './About.vue'
import InviteUsers from './InviteUsers.vue'
// import { useAuthStore } from '@/store'
import { SvgIcon } from '@/components/common'
interface Props {
visible: boolean
}
interface Emit {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
// const authStore = useAuthStore()
// const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
const active = ref('General')
const show = computed({
get() {
return props.visible
},
set(visible: boolean) {
emit('update:visible', visible)
},
})
</script>
<template>
<NModal v-model:show="show" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
<div>
<NTabs v-model:value="active" type="line" animated>
<NTabPane name="General" tab="General">
<template #tab>
<SvgIcon class="text-lg" icon="ri:file-user-line" />
<span class="ml-2">设置</span>
</template>
<div class="min-h-[100px]">
<UserInfo />
</div>
</NTabPane>
<!-- <NTabPane v-if="isChatGPTAPI" name="Advanced" tab="Advanced">
<template #tab>
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
<span class="ml-2">{{ $t('setting.advanced') }}</span>
</template>
<div class="min-h-[100px]">
<Advanced />
</div>
</NTabPane> -->
<NTabPane name="InviteUsers" tab="InviteUsers">
<template #tab>
<SvgIcon class="text-lg" icon="mdi:invite" />
<span class="ml-2">邀请新用户</span>
</template>
<InviteUsers />
</NTabPane>
<NTabPane name="About" tab="about">
<template #tab>
<SvgIcon class="text-lg" icon="mdi:about-circle-outline" />
<span class="ml-2">关于</span>
</template>
<About />
</NTabPane>
</NTabs>
</div>
</NModal>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script setup lang='ts'>
import { computed, useAttrs } from 'vue'
import { Icon } from '@iconify/vue'
interface Props {
icon?: string
}
defineProps<Props>()
const attrs = useAttrs()
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || '',
}))
</script>
<template>
<Icon :icon="icon ?? ''" v-bind="bindAttrs" />
</template>
@@ -0,0 +1,72 @@
<script setup lang='ts'>
import { computed, ref, watch } from 'vue'
import { NButton, NInput, NModal, useMessage } from 'naive-ui'
import { Captcha } from '@/components/common'
const props = defineProps<{
visible: boolean
verificationId: string
loading?: boolean
title?: string
}>()
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void
(e: 'update:loading', loading: boolean): void
(e: 'done', id: number): void// 创建完成
(e: 'onSubmit', verificationId: string, vCode: string, obj: { refresh: () => void }): void // 提交
}>()
const ms = useMessage()
const vCode = ref<string>('')
const captchaRef = ref()
// 更新值父组件传来的值
const show = computed({
get: () => props.visible,
set: (visible: boolean) => {
emit('update:visible', visible)
},
})
watch(show, (newValue, oldValue) => {
if (newValue === true) {
captchaRef.value?.refresh()
vCode.value = ''
emit('update:loading', false)
}
})
// 提交
function handleSubmit() {
if (vCode.value === '') {
ms.warning('验证码必须填写')
return
}
if (!props.loading) {
emit('onSubmit', props.verificationId, vCode.value, {
refresh() {
captchaRef.value?.refresh()
vCode.value = ''
},
})
}
}
</script>
<template>
<div>
<NModal v-model:show="show" preset="card" style="width: 400px" :title="title ?? '请输入验证码继续'" :mask-closable="false">
<Captcha ref="captchaRef" class="rounded border" :src="`/api/captcha/getImageByCaptchaId/${verificationId}/200/60?0`" />
<div class="flex">
<div class="flex w-[80%]">
<NInput v-model:value="vCode" placeholder="输入图中字母或数字后继续" @keydown.enter="handleSubmit" />
</div>
<div class="flex ml-[auto]">
<NButton type="info" :loading="loading" :disabled="loading" @click="handleSubmit">
继续
</NButton>
</div>
</div>
</NModal>
</div>
</template>
+19
View File
@@ -0,0 +1,19 @@
import HoverButton from './HoverButton/index.vue'
import SvgIcon from './SvgIcon/index.vue'
import Setting from './Setting/index.vue'
import Captcha from './Captcha/index.vue'
import Verification from './Verification/index.vue'
import ItemIcon from './ItemIcon/index.vue'
import NaiveProvider from './NaiveProvider/index.vue'
import RoundCardModal from './RoundCardModal/index.vue'
export {
Verification,
HoverButton,
SvgIcon,
Setting,
Captcha,
ItemIcon,
NaiveProvider,
RoundCardModal,
}
+8
View File
@@ -0,0 +1,8 @@
<template>
<div class="text-neutral-400">
<span>Star on</span>
<a href="https://github.com/Chanzhaoyu/chatgpt-bot" target="_blank" class="text-blue-500">
GitHub
</a>
</div>
</template>
+3
View File
@@ -0,0 +1,3 @@
import GithubSite from './GithubSite.vue'
export { GithubSite }
+74
View File
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps<{
hideSecond?: boolean
}>()
interface CurrentDate {
time: string
date: string
week: string
}
const currentDate = ref<CurrentDate>({
time: '--:--',
date: '------',
week: '--',
})
function updateCurrentDate() {
const now = new Date()
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
if (!props.hideSecond) {
const seconds = String(now.getSeconds()).padStart(2, '0')
currentDate.value.time = `${hours}:${minutes}:${seconds}`
}
else {
currentDate.value.time = `${hours}:${minutes}`
}
// 获取当前的日期
const day = now.getDate()
const month = now.getMonth() + 1 // 月份从0开始,所以要加1
// const year = now.getFullYear()
const daysOfWeek = ['日', '一', '二', '三', '四', '五', '六']
// const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
currentDate.value.week = daysOfWeek[now.getDay()]
currentDate.value.date = `${month}-${day}`
}
const updateClock = () => {
updateCurrentDate()
}
const intervalId = setInterval(updateClock, 1000)
onMounted(() => {
updateClock()
updateCurrentDate()
})
onBeforeUnmount(() => {
clearInterval(intervalId)
})
</script>
<template>
<div class="w-full text-center">
<span class="text-3xl font-[600]">
{{ currentDate.time }}
</span>
<div>
<span>
{{ currentDate.date }}
</span>
<span>
星期{{ currentDate.week }}
</span>
</div>
</div>
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { NInput } from 'naive-ui'
import { SvgIcon } from '@/components/common'
</script>
<template>
<div class="w-full">
<NInput size="large" class="background" round placeholder="输入搜索内容">
<template #prefix>
百度
</template>
<template #suffix>
<SvgIcon icon="iconamoon:search-fill" />
</template>
</NInput>
</div>
</template>
<style scoped>
.background{
background-color: #ffffff78;
}
</style>
+4
View File
@@ -0,0 +1,4 @@
import Clock from './Clock/index.vue'
import SearchBox from './SearchBox/index.vue'
export { Clock, SearchBox }