test
shenhan 2025-01-20 16:15:12 +08:00
parent 44463a89e0
commit ee9fefdd9d
14 changed files with 724 additions and 339 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

13
app/auto-imports.d.ts vendored 100644
View File

@ -0,0 +1,13 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const useDialog: typeof import('naive-ui')['useDialog']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useNotification: typeof import('naive-ui')['useNotification']
}

27
app/components.d.ts vendored 100644
View File

@ -0,0 +1,27 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAvatar: typeof import('naive-ui')['NAvatar']
NBadge: typeof import('naive-ui')['NBadge']
NButton: typeof import('naive-ui')['NButton']
NDropdown: typeof import('naive-ui')['NDropdown']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormTtem: typeof import('naive-ui')['NFormTtem']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputgroup: typeof import('naive-ui')['NInputgroup']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NModal: typeof import('naive-ui')['NModal']
NQrCode: typeof import('naive-ui')['NQrCode']
NSpace: typeof import('naive-ui')['NSpace']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View File

@ -1,125 +1,260 @@
<!-- components/LoginModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import {
NModal,
NCard,
NForm,
NFormItem,
NInput,
NButton,
createDiscreteApi
} from 'naive-ui'
// 使 createDiscreteApi useMessage
import phoneImg from "@/assets/img/phone.png";
import wechatImg from "@/assets/img/wechat.png";
import type { FormInst, FormRules } from "naive-ui";
import { createDiscreteApi } from 'naive-ui';
import { ref } from "vue";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const { message } = createDiscreteApi(['message'])
const userStore = useUserStore()
const visible = ref(false)
const loading = ref(false)
const formRef = ref(null)
const formModel = ref({
username: '',
password: ''
})
const rules = {
username: {
required: true,
message: '请输入用户名',
trigger: 'blur'
},
password: {
required: true,
message: '请输入密码',
trigger: 'blur'
}
}
//
function showModal() {
visible.value = true
}
//
function hideModal() {
visible.value = false
formModel.value = {
username: '',
password: ''
}
}
//
async function handleLogin() {
if (!formRef.value) return
try {
loading.value = true
await formRef.value.validate()
// API
// const res = await login(formModel.value)
//
userStore.login('fake-token')
message.success('登录成功')
hideModal()
} catch (err) {
console.error(err)
} finally {
loading.value = false
}
}
//
defineExpose({
showModal
})
showModal,
});
const rules: FormRules = {
phone: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{ pattern: /^1[3-9]\d{9}$/, message: "手机号格式不正确", trigger: "blur" },
],
code: [
{ required: true, message: "请输入验证码", trigger: "blur" },
{ len: 4, message: "验证码长度为4位", trigger: "blur" },
],
};
//
const isVisible = ref(false);
const formRef = ref<FormInst | null>(null)
type LoginMode = "wechat" | "phone";
const currentLoginMode = ref<LoginMode>("phone");
const codeButtonText = ref("获取验证码");
const codeButtonDisabled = ref(false);
const timerCode: Ref<ReturnType<typeof setInterval> | null> = ref(null);
interface UserData {
code:number,
phone:number,
}
interface ApiResponse<T> {
code: number;
msg: string;
token: string;
}
const formData = ref({
});
interface LoginModeDescription {
title: string;
des: string;
src: string;
}
type LoginModeDescriptions = Record<LoginMode, LoginModeDescription>;
//
const loginModeDescriptions: LoginModeDescriptions = {
phone: {
title: "登录",
des: "如未注册,验证后自动登录",
src: wechatImg,
},
wechat: {
title: "微信一键登录",
des: "关注后自动登录",
src: phoneImg,
},
};
function showModal() {
if (isVisible.value) return;
isVisible.value = true;
}
function onCloseLogin() {
formData.value = {}
isVisible.value = false;
}
//
function toggleMode() {
return currentLoginMode.value === "phone" ? "wechat" : "phone";
}
function setCurrentLoginMode() {
currentLoginMode.value = toggleMode();
}
interface UserToken{
token:string
}
//
async function handleValidateClick(e: MouseEvent) {
e.preventDefault()
formRef.value?.validate(async(errors:any) => {
if (!errors) {
const res = await request.post<ApiResponse<UserData>>('/phoneLogin', {
...formData.value
})
userStore.setToken(res.token)
const res1 = await request.get<ApiResponse<UserToken>>('/getInfo', {
token:res.token
})
userStore.setUserInfo(res1.user)
onCloseLogin()
window.location.href = '/'
}
else {
console.log(errors)
}
})
}
interface codeInterface{
phone:number
}
//
async function onGetCode() {
try {
const response = await request.get<codeInterface>("/getCode", {
params: {
phone:formData.value.phone
}
});
if (response.code === 200) {
codeButtonDisabled.value = true;
let countdown = 60;
codeButtonText.value = `${countdown}秒后重试`;
if (timerCode.value) clearInterval(timerCode.value);
timerCode.value = setInterval(() => {
countdown--;
if (countdown <= 0) {
clearInterval(timerCode.value);
codeButtonDisabled.value = false;
codeButtonText.value = "获取验证码";
} else {
codeButtonText.value = `${countdown}秒后重试`;
}
}, 1000);
} else {
message.error(response.message || "获取验证码失败!");
}
} catch (err) {
console.error(err);
}
}
const onCloseModel = () =>{
formData.value = {}
if (timerCode.value) {
clearInterval(timerCode.value);
}
}
//
onUnmounted(() => {
formData.value = {}
if (timerCode.value) {
clearInterval(timerCode.value);
}
});
</script>
<template>
<NModal
v-model:show="visible"
:on-after-leave="onCloseModel"
v-model:show="isVisible"
preset="card"
style="width: 400px"
:maskClosable="false"
title="登录"
>
<NForm
ref="formRef"
:model="formModel"
:rules="rules"
>
<NFormItem label="用户名" path="username">
<NInput
v-model:value="formModel.username"
placeholder="请输入用户名"
/>
</NFormItem>
<NFormItem label="密码" path="password">
<NInput
v-model:value="formModel.password"
type="password"
placeholder="请输入密码"
@keyup.enter="handleLogin"
/>
</NFormItem>
</NForm>
<template #footer>
<div class="flex justify-end gap-2">
<NButton @click="hideModal"></NButton>
<NButton
type="primary"
:loading="loading"
@click="handleLogin"
<div>
<!-- 关闭按钮 -->
<!-- <div
class="center fixed right-5 top-5 z-999 h-5 w-5 cursor-pointer b-white" @click="onCloseLogin(false)"
>
<NIcon size="30" class="text-white">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><g><g><polygon
points="405,136.798 375.202,107 256,226.202 136.798,107 107,136.798 226.202,256 107,375.202 136.798,405 256,285.798
375.202,405 405,375.202 285.798,256"
/></g></g></svg>
</NIcon>
</div> -->
<!-- 登录表单区域 -->
<div>
<h2 class="text-center text-xl text-gray-800 font-bold">
{{ loginModeDescriptions[currentLoginMode].title }}
</h2>
<p class="text-center text-sm text-gray-500">
{{ loginModeDescriptions[currentLoginMode].des }}
</p>
<!-- 手机号登录表单 -->
<n-form
v-if="currentLoginMode === 'phone'"
ref="formRef"
:model="formData"
:rules="rules"
label-placement="left"
label-width="70px"
size="large"
>
登录
</NButton>
<n-form-item label="手机号" path="phone">
<n-input v-model:value="formData.phone" placeholder="输入手机号" />
</n-form-item>
<n-form-item label="密码" path="code">
<n-input-group>
<n-input v-model:value="formData.code" placeholder="输入密码" />
<n-button
type="primary"
:disabled="codeButtonDisabled"
ghost
@click="onGetCode"
>
{{ codeButtonText }}
</n-button>
</n-input-group>
</n-form-item>
<n-form-item>
<n-button
type="primary"
block
attr-type="button"
@click="handleValidateClick"
>
登录
</n-button>
</n-form-item>
</n-form>
<!-- 微信登录区域 -->
<div v-else>
<div class="flex justify-center">
<WechatLoginQr />
</div>
</div>
</div>
</template>
<!-- 其他登录方式 -->
<div class="text-center">
<div class="text-sm text-gray-400">其他登录方式</div>
<div class="mt-2 flex items-center justify-center gap-6">
<div
class="h-10 w-10 cursor-pointer rounded-full p-2 transition-all hover:bg-gray-100"
@click="setCurrentLoginMode"
>
<img
:src="loginModeDescriptions[currentLoginMode].src"
alt=""
class="h-10 w-10"
/>
</div>
</div>
</div>
</div>
</NModal>
</template>
</template>
<style scoped></style>

View File

@ -0,0 +1,120 @@
<script lang="ts" setup>
// import { useRouter } from 'vue-router'
// import { getUUid, uuidLogin } from '@api/login'
// import { useStore } from '@store/index'
// import { IosRefresh } from '@vicons/ionicons5';
import { RotateCcw } from 'lucide-vue-next';
import { onBeforeUnmount, onMounted, ref } from 'vue';
//
const emit = defineEmits<{
(event: 'login-success', data: any): void
}>()
interface UserToken{
token:string
}
interface ApiResponse<T> {
code: number;
msg: string;
token: string;
}
const qrUrl = ref<string>('')
const qrSize = ref(174)
// const router = useRouter()
// const store = useStore()
let pollingTimer: ReturnType<typeof setInterval> | undefined
const bindTimeout = ref(false)
async function onGetUUid() {
bindTimeout.value = false
try {
// /wx/uuid/get
const res = await request.get('/wx/uuid/get')
if (res.code === 200) {
const appid = 'wx82d4c3c96f0ffa5b'
const { uuid } = res
const redirect_uri = `http://rtec8z.natappfree.cc/wx/uuid/bind/openid?uuid=${uuid}`
const codeUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${encodeURIComponent(
redirect_uri,
)}&response_type=code&scope=snsapi_userinfo&state=123456#wechat_redirect`
qrUrl.value = codeUrl
let counter = 1
pollingTimer && clearTimeout(pollingTimer)
pollingTimer = setInterval(async() => {
await request.get('/wx/uuid/login',{uuid}).then(async(res)=>{
counter++
if (counter === 59) {
clearTimeout(pollingTimer)
bindTimeout.value = true
}
if (res.status === 1) {
clearTimeout(pollingTimer)
const userStore = useUserStore()
userStore.setToken(res.token)
const res1 = await request.get<ApiResponse<UserToken>>('/getInfo', {
token:res.token
})
userStore.setUserInfo(res1.user)
window.location.href = '/'
// const parent = getCurrentInstance().parent
// parent.exposed.onCloseLogin()
// store.userInfo = res.data
// router.push('./home')
// store.dispatch("uuidLogin", res)
// that.$store.dispatch("uuidLogin", res)
// setTimeout(() => {
// that.$router.push({
// path: that.redirect || "/"
// }).catch(() => {});
// }, 1500)
}
}) .catch((err: any) => {
console.log(err)
clearTimeout(pollingTimer)
})
}, 1000)
}
}
catch (error) {
console.log(error)
}
}
//
// const handleLoginSuccess = (data: any) => {
// //
// emit('login-success', data)
// }
//
onMounted(() => {
onGetUUid()
})
onBeforeUnmount(() => {
clearInterval(pollingTimer)
})
</script>
<template>
<div class="w-[280px] px-5 text-center relative">
<div v-if="bindTimeout" class="absolute left-[41px] h-full w-[230px] top-0 z-[9999] flex items-center justify-center flex-col cursor-pointer text-white bg-black/40" @click="onGetUUid">
刷新二维码
<rotate-ccw/>
</div>
<div v-if="qrUrl" class="relative w-full">
<component
is="RotateCcw"
class="h-[18px] w-[18px] color-white"
/>
<n-qr-code :value="qrUrl" :size="qrSize" />
</div>
<div v-else class="p-5 text-gray-400">
加载中...
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -3,20 +3,11 @@ import {
Bell, Binary, Code2, Crown, GraduationCap, Image, LayoutGrid,
Lightbulb, Maximize, Monitor, Plus, Search, User, Workflow
} from 'lucide-vue-next';
import {
NAvatar,
NBadge, NButton, NDropdown, NInput, NSpace
} from 'naive-ui';
onMounted(() => {
const userStore = useUserStore()
userStore.checkLoginStatus()
})
import { ref, onMounted } from 'vue'
const isClient = ref(false)
const modalStore = useModalStore()
// import ../assets/img/default-avatar.png
import defaultAvatar from '../assets/img/default-avatar.png'
//
const iconMap: any = {
'/model-square': LayoutGrid,
@ -38,6 +29,9 @@ const route = useRoute()
const menuStore = useMenuStore()
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
//
watch(
() => route.path,
@ -46,9 +40,9 @@ watch(
},
{ immediate: true } //
)
onMounted(() => {
isClient.value = true
})
//
menuStore.menuItems = menuStore.menuItems.map(item => ({
...item,
@ -68,24 +62,44 @@ const notificationOptions = [
]
//
const userOptions = [
const userOptions = ref([
{
label: '个人中心',
key: 'profile'
label: '我的模型',
key: 'model'
},
{
label: '创作中心',
key: 'creator'
label: '我的作品',
key: 'project'
},
{
type: 'divider',
key: 'd1'
label: '我的点赞',
key: 'like'
},
{
label: '账号设置',
key: 'userSettings'
},
{
label: '退出登录',
key: 'logout'
}
]
])
const handleUserSelect = async(key: string) => {
if (key === 'logout') {
try {
await request.post('/logout')
userStore.logout()
window.location.href = '/'
} catch (error) {
console.error('Logout failed:', error)
}
}
}
const handleLogin = () => {
modalStore.showLoginModal()
}
</script>
<template>
@ -152,13 +166,17 @@ const userOptions = [
</NDropdown>
<!-- User -->
<NDropdown :options="userOptions" trigger="click">
<NDropdown v-if="isClient && userStore.token" :options="userOptions" :on-select="handleUserSelect" trigger="hover" >
<NAvatar
class="cursor-pointer w-10 h-10"
round
size="small"
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
:src="userStore.userInfo && userStore.userInfo.avatar ? userStore.userInfo.avatar : defaultAvatar"
/>
</NDropdown>
<div v-else>
<div @click="handleLogin" class="text-white bg-[#197dff] rounded-[4px] px-4 py-2 text-xs cursor-pointer hover:bg-[#1a6eff]">登录/注册</div>
</div>
</NSpace>
</header>

View File

@ -14,7 +14,7 @@ export default defineNuxtRouteMiddleware((to) => {
// 需要登录权限的路由列表
const authRoutes = [
'/high-availability',
'/member-center',
// 可以继续添加其他需要登录的路由
]

View File

@ -1,18 +1,26 @@
// stores/user.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import {
parse,
stringify,
} from 'zipson'
export const useUserStore = defineStore('user', () => {
const isLoggedIn = ref(false)
const token = ref('')
const userInfo = ref({})
// 模拟登录
function login(userToken: string) {
function setToken(userToken: string) {
isLoggedIn.value = true
token.value = userToken
// 可以存储到 localStorage
localStorage.setItem('token', userToken)
}
function setUserInfo(info: any) {
userInfo.value = info
}
// 登出
function logout() {
isLoggedIn.value = false
@ -32,8 +40,67 @@ export const useUserStore = defineStore('user', () => {
return {
isLoggedIn,
token,
login,
logout,
checkLoginStatus
checkLoginStatus,
setToken,
setUserInfo,
userInfo,
}
})
}, {
persist: {
storage: {
getItem(key) {
return window.localStorage.getItem(key)
},
setItem(key, value) {
window.localStorage.setItem(key, value)
},
},
serializer: {
deserialize: parse,
serialize: stringify,
},
},
})
// import { defineStore } from 'pinia'
// interface State {
// token: string
// userInfo: Record<string, any> // 或者定义一个更具体的类型
// }
// export const useUserStore = defineStore('user', {
// state: (): State => ({
// token: '',
// userInfo: {},
// }),
// actions: {
// setToken(token: string) {
// this.token = token
// debugger
// },
// setUserInfo(userInfo: Record<string, any>) {
// this.userInfo = userInfo
// debugger
// },
// // logout() {
// // getLogout().then(() => {
// // this.userInfo = {}
// // this.token = ''
// // })
// // }
// },
// getters: {
// // 定义计算属性
// },
// persist: {
// enabled: true,
// strategies: [
// {
// // key: 'userInfo',
// Storage: localStorage,
// paths: ['userInfo', 'token'],
// },
// ],
// },
// })

View File

@ -1,8 +1,7 @@
// utils/request.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import { createDiscreteApi } from 'naive-ui'
const { message, loadingBar } = createDiscreteApi(['message', 'loadingBar'])
// 定义响应数据接口
@ -26,6 +25,12 @@ class RequestHttp {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
const userStore = useUserStore()
const isToken = (config.headers || {}).isToken === false
if (userStore.token && !isToken) {
config.headers['Authorization'] = 'Bearer ' + userStore.token // 让每个请求携带自定义token 请根据实际情况自行修改
}
// 开启 loading
if (config.loading) {
loadingBar.start()

View File

@ -1,8 +1,11 @@
import AutoImport from 'unplugin-auto-import/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import { pwa } from './app/config/pwa'
import { appDescription } from './app/constants/index'
export default defineNuxtConfig({
ssr: true,
modules: [
'@vueuse/nuxt',
'@unocss/nuxt',
@ -10,39 +13,15 @@ export default defineNuxtConfig({
'@nuxtjs/color-mode',
'@vite-pwa/nuxt',
'@nuxt/eslint',
'nuxtjs-naive-ui',
'@pinia-plugin-persistedstate/nuxt',
],
routeRules: {
'/': { redirect: '/model-square' }
},
nitro: {
devProxy: {
'/api': {
target: 'http://example.com',
changeOrigin: true,
prependPath: true
}
}
},
ssr: true,
devtools: {
enabled: true,
},
build: {
transpile:
process.env.NODE_ENV === 'production'
? ['naive-ui', 'vueuc', '@css-render/vue3-ssr', '@juggle/resize-observer']
: ['@juggle/resize-observer']
},
vite: {
define: {
'process.env.DEBUG': false,
},
// 避免 vite 热更新时出现警告
// optimizeDeps: {
// include: ['date-fns-tz/esm/formatInTimeZone']
// }
},
app: {
head: {
viewport: 'width=device-width,initial-scale=1',
@ -69,6 +48,16 @@ export default defineNuxtConfig({
classSuffix: '',
},
build: {
transpile:
process.env.NODE_ENV === 'production'
? ['naive-ui', 'vueuc', '@css-render/vue3-ssr', '@juggle/resize-observer']
: ['@juggle/resize-observer'],
},
routeRules: {
'/': { redirect: '/model-square' },
},
future: {
compatibilityVersion: 4,
},
@ -84,6 +73,15 @@ export default defineNuxtConfig({
compatibilityDate: '2024-08-14',
nitro: {
devProxy: {
'/api': {
// 192.168.1.69 海洋
// 192.168.2.22 代
target: `http://192.168.2.22:8080`,
changeOrigin: true,
prependPath: true,
},
},
esbuild: {
options: {
target: 'esnext',
@ -95,6 +93,32 @@ export default defineNuxtConfig({
ignore: ['/hi'],
},
},
vite: {
define: {
'process.env.DEBUG': false,
},
plugins: [
AutoImport({
imports: [
{
'naive-ui': [
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar',
],
},
],
}),
Components({
resolvers: [NaiveUiResolver()],
}),
],
// 避免 vite 热更新时出现警告
// optimizeDeps: {
// include: ['date-fns-tz/esm/formatInTimeZone']
// }
},
eslint: {
config: {

View File

@ -22,6 +22,7 @@
"@nuxt/eslint": "^0.7.4",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.13.1",
"@pinia-plugin-persistedstate/nuxt": "^1.2.1",
"@pinia/nuxt": "^0.9.0",
"@types/node": "^22.10.6",
"@unocss/eslint-config": "^0.65.3",
@ -32,11 +33,15 @@
"consola": "^3.3.1",
"eslint": "^9.17.0",
"eslint-plugin-format": "^0.1.3",
"naive-ui": "^2.41.0",
"nuxt": "^3.15.0",
"nuxtjs-naive-ui": "^1.0.2",
"pinia": "^2.3.0",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vue-tsc": "^2.2.0",
"vueuc": "^0.4.64"
},
@ -50,6 +55,8 @@
"axios": "^1.7.9",
"date-fns-tz": "^3.2.0",
"lucide-vue-next": "^0.471.0",
"naive-ui": "^2.41.0"
"naive-ui": "^2.41.0",
"pinia-plugin-persistedstate": "^4.2.0",
"zipson": "^0.2.12"
}
}

File diff suppressed because it is too large Load Diff