test
parent
44463a89e0
commit
ee9fefdd9d
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 |
|
@ -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']
|
||||||
|
}
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,125 +1,260 @@
|
||||||
<!-- components/LoginModal.vue -->
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import phoneImg from "@/assets/img/phone.png";
|
||||||
import {
|
import wechatImg from "@/assets/img/wechat.png";
|
||||||
NModal,
|
import type { FormInst, FormRules } from "naive-ui";
|
||||||
NCard,
|
import { createDiscreteApi } from 'naive-ui';
|
||||||
NForm,
|
import { ref } from "vue";
|
||||||
NFormItem,
|
import { useUserStore } from "@/stores/user";
|
||||||
NInput,
|
const userStore = useUserStore();
|
||||||
NButton,
|
|
||||||
createDiscreteApi
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
// 使用 createDiscreteApi 替代 useMessage
|
|
||||||
const { message } = createDiscreteApi(['message'])
|
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({
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NModal
|
<NModal
|
||||||
v-model:show="visible"
|
:on-after-leave="onCloseModel"
|
||||||
|
v-model:show="isVisible"
|
||||||
preset="card"
|
preset="card"
|
||||||
style="width: 400px"
|
style="width: 400px"
|
||||||
:maskClosable="false"
|
:maskClosable="false"
|
||||||
title="登录"
|
|
||||||
>
|
>
|
||||||
<NForm
|
<div>
|
||||||
ref="formRef"
|
<!-- 关闭按钮 -->
|
||||||
:model="formModel"
|
<!-- <div
|
||||||
:rules="rules"
|
class="center fixed right-5 top-5 z-999 h-5 w-5 cursor-pointer b-white" @click="onCloseLogin(false)"
|
||||||
>
|
>
|
||||||
<NFormItem label="用户名" path="username">
|
<NIcon size="30" class="text-white">
|
||||||
<NInput
|
<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
|
||||||
v-model:value="formModel.username"
|
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
|
||||||
placeholder="请输入用户名"
|
375.202,405 405,375.202 285.798,256"
|
||||||
/>
|
/></g></g></svg>
|
||||||
</NFormItem>
|
</NIcon>
|
||||||
<NFormItem label="密码" path="password">
|
</div> -->
|
||||||
<NInput
|
|
||||||
v-model:value="formModel.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入密码"
|
|
||||||
@keyup.enter="handleLogin"
|
|
||||||
/>
|
|
||||||
</NFormItem>
|
|
||||||
</NForm>
|
|
||||||
|
|
||||||
<template #footer>
|
<!-- 登录表单区域 -->
|
||||||
<div class="flex justify-end gap-2">
|
<div>
|
||||||
<NButton @click="hideModal">取消</NButton>
|
<h2 class="text-center text-xl text-gray-800 font-bold">
|
||||||
<NButton
|
{{ loginModeDescriptions[currentLoginMode].title }}
|
||||||
type="primary"
|
</h2>
|
||||||
:loading="loading"
|
<p class="text-center text-sm text-gray-500">
|
||||||
@click="handleLogin"
|
{{ loginModeDescriptions[currentLoginMode].des }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 手机号登录表单 -->
|
||||||
|
<n-form
|
||||||
|
v-if="currentLoginMode === 'phone'"
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="70px"
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
登录
|
<n-form-item label="手机号" path="phone">
|
||||||
</NButton>
|
<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>
|
</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>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
|
@ -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>
|
|
@ -3,20 +3,11 @@ import {
|
||||||
Bell, Binary, Code2, Crown, GraduationCap, Image, LayoutGrid,
|
Bell, Binary, Code2, Crown, GraduationCap, Image, LayoutGrid,
|
||||||
Lightbulb, Maximize, Monitor, Plus, Search, User, Workflow
|
Lightbulb, Maximize, Monitor, Plus, Search, User, Workflow
|
||||||
} from 'lucide-vue-next';
|
} from 'lucide-vue-next';
|
||||||
import {
|
import { ref, onMounted } from 'vue'
|
||||||
NAvatar,
|
const isClient = ref(false)
|
||||||
NBadge, NButton, NDropdown, NInput, NSpace
|
const modalStore = useModalStore()
|
||||||
} from 'naive-ui';
|
// import ../assets/img/default-avatar.png
|
||||||
|
import defaultAvatar from '../assets/img/default-avatar.png'
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const userStore = useUserStore()
|
|
||||||
userStore.checkLoginStatus()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 路径到图标的映射
|
// 路径到图标的映射
|
||||||
const iconMap: any = {
|
const iconMap: any = {
|
||||||
'/model-square': LayoutGrid,
|
'/model-square': LayoutGrid,
|
||||||
|
@ -38,6 +29,9 @@ const route = useRoute()
|
||||||
|
|
||||||
const menuStore = useMenuStore()
|
const menuStore = useMenuStore()
|
||||||
|
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// 监听路由变化
|
// 监听路由变化
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
|
@ -46,9 +40,9 @@ watch(
|
||||||
},
|
},
|
||||||
{ immediate: true } // 这样一进入页面就会执行一次
|
{ immediate: true } // 这样一进入页面就会执行一次
|
||||||
)
|
)
|
||||||
|
onMounted(() => {
|
||||||
|
isClient.value = true
|
||||||
|
})
|
||||||
// 更新菜单项中的图标
|
// 更新菜单项中的图标
|
||||||
menuStore.menuItems = menuStore.menuItems.map(item => ({
|
menuStore.menuItems = menuStore.menuItems.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
|
@ -68,24 +62,44 @@ const notificationOptions = [
|
||||||
]
|
]
|
||||||
|
|
||||||
// 用户下拉选项
|
// 用户下拉选项
|
||||||
const userOptions = [
|
const userOptions = ref([
|
||||||
{
|
{
|
||||||
label: '个人中心',
|
label: '我的模型',
|
||||||
key: 'profile'
|
key: 'model'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创作中心',
|
label: '我的作品',
|
||||||
key: 'creator'
|
key: 'project'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'divider',
|
label: '我的点赞',
|
||||||
key: 'd1'
|
key: 'like'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '账号设置',
|
||||||
|
key: 'userSettings'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '退出登录',
|
label: '退出登录',
|
||||||
key: 'logout'
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -152,13 +166,17 @@ const userOptions = [
|
||||||
</NDropdown>
|
</NDropdown>
|
||||||
|
|
||||||
<!-- User -->
|
<!-- User -->
|
||||||
<NDropdown :options="userOptions" trigger="click">
|
<NDropdown v-if="isClient && userStore.token" :options="userOptions" :on-select="handleUserSelect" trigger="hover" >
|
||||||
<NAvatar
|
<NAvatar
|
||||||
|
class="cursor-pointer w-10 h-10"
|
||||||
round
|
round
|
||||||
size="small"
|
size="small"
|
||||||
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
|
:src="userStore.userInfo && userStore.userInfo.avatar ? userStore.userInfo.avatar : defaultAvatar"
|
||||||
/>
|
/>
|
||||||
</NDropdown>
|
</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>
|
</NSpace>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default defineNuxtRouteMiddleware((to) => {
|
||||||
|
|
||||||
// 需要登录权限的路由列表
|
// 需要登录权限的路由列表
|
||||||
const authRoutes = [
|
const authRoutes = [
|
||||||
'/high-availability',
|
'/member-center',
|
||||||
// 可以继续添加其他需要登录的路由
|
// 可以继续添加其他需要登录的路由
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
// stores/user.ts
|
// stores/user.ts
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
parse,
|
||||||
|
stringify,
|
||||||
|
} from 'zipson'
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
const isLoggedIn = ref(false)
|
const isLoggedIn = ref(false)
|
||||||
const token = ref('')
|
const token = ref('')
|
||||||
|
const userInfo = ref({})
|
||||||
|
|
||||||
// 模拟登录
|
function setToken(userToken: string) {
|
||||||
function login(userToken: string) {
|
|
||||||
isLoggedIn.value = true
|
isLoggedIn.value = true
|
||||||
token.value = userToken
|
token.value = userToken
|
||||||
// 可以存储到 localStorage
|
|
||||||
localStorage.setItem('token', userToken)
|
localStorage.setItem('token', userToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setUserInfo(info: any) {
|
||||||
|
userInfo.value = info
|
||||||
|
}
|
||||||
|
|
||||||
// 登出
|
// 登出
|
||||||
function logout() {
|
function logout() {
|
||||||
isLoggedIn.value = false
|
isLoggedIn.value = false
|
||||||
|
@ -32,8 +40,67 @@ export const useUserStore = defineStore('user', () => {
|
||||||
return {
|
return {
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
token,
|
token,
|
||||||
login,
|
|
||||||
logout,
|
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'],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// utils/request.ts
|
// utils/request.ts
|
||||||
import axios from 'axios'
|
|
||||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import axios from 'axios'
|
||||||
import { createDiscreteApi } from 'naive-ui'
|
import { createDiscreteApi } from 'naive-ui'
|
||||||
|
|
||||||
const { message, loadingBar } = createDiscreteApi(['message', 'loadingBar'])
|
const { message, loadingBar } = createDiscreteApi(['message', 'loadingBar'])
|
||||||
|
|
||||||
// 定义响应数据接口
|
// 定义响应数据接口
|
||||||
|
@ -26,6 +25,12 @@ class RequestHttp {
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const isToken = (config.headers || {}).isToken === false
|
||||||
|
if (userStore.token && !isToken) {
|
||||||
|
config.headers['Authorization'] = 'Bearer ' + userStore.token // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||||
|
}
|
||||||
|
|
||||||
// 开启 loading
|
// 开启 loading
|
||||||
if (config.loading) {
|
if (config.loading) {
|
||||||
loadingBar.start()
|
loadingBar.start()
|
||||||
|
|
|
@ -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 { pwa } from './app/config/pwa'
|
||||||
import { appDescription } from './app/constants/index'
|
import { appDescription } from './app/constants/index'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: true,
|
|
||||||
modules: [
|
modules: [
|
||||||
'@vueuse/nuxt',
|
'@vueuse/nuxt',
|
||||||
'@unocss/nuxt',
|
'@unocss/nuxt',
|
||||||
|
@ -10,39 +13,15 @@ export default defineNuxtConfig({
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
'@vite-pwa/nuxt',
|
'@vite-pwa/nuxt',
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
|
'nuxtjs-naive-ui',
|
||||||
|
'@pinia-plugin-persistedstate/nuxt',
|
||||||
],
|
],
|
||||||
routeRules: {
|
ssr: true,
|
||||||
'/': { redirect: '/model-square' }
|
|
||||||
},
|
|
||||||
nitro: {
|
|
||||||
devProxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://example.com',
|
|
||||||
changeOrigin: true,
|
|
||||||
prependPath: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
devtools: {
|
devtools: {
|
||||||
enabled: true,
|
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: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
viewport: 'width=device-width,initial-scale=1',
|
viewport: 'width=device-width,initial-scale=1',
|
||||||
|
@ -69,6 +48,16 @@ export default defineNuxtConfig({
|
||||||
classSuffix: '',
|
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: {
|
future: {
|
||||||
compatibilityVersion: 4,
|
compatibilityVersion: 4,
|
||||||
},
|
},
|
||||||
|
@ -84,6 +73,15 @@ export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-08-14',
|
compatibilityDate: '2024-08-14',
|
||||||
|
|
||||||
nitro: {
|
nitro: {
|
||||||
|
devProxy: {
|
||||||
|
'/api': {
|
||||||
|
// 192.168.1.69 海洋
|
||||||
|
// 192.168.2.22 代
|
||||||
|
target: `http://192.168.2.22:8080`,
|
||||||
|
changeOrigin: true,
|
||||||
|
prependPath: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
esbuild: {
|
esbuild: {
|
||||||
options: {
|
options: {
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
|
@ -95,6 +93,32 @@ export default defineNuxtConfig({
|
||||||
ignore: ['/hi'],
|
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: {
|
eslint: {
|
||||||
config: {
|
config: {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"@nuxt/eslint": "^0.7.4",
|
"@nuxt/eslint": "^0.7.4",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@nuxtjs/tailwindcss": "^6.13.1",
|
"@nuxtjs/tailwindcss": "^6.13.1",
|
||||||
|
"@pinia-plugin-persistedstate/nuxt": "^1.2.1",
|
||||||
"@pinia/nuxt": "^0.9.0",
|
"@pinia/nuxt": "^0.9.0",
|
||||||
"@types/node": "^22.10.6",
|
"@types/node": "^22.10.6",
|
||||||
"@unocss/eslint-config": "^0.65.3",
|
"@unocss/eslint-config": "^0.65.3",
|
||||||
|
@ -32,11 +33,15 @@
|
||||||
"consola": "^3.3.1",
|
"consola": "^3.3.1",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-format": "^0.1.3",
|
"eslint-plugin-format": "^0.1.3",
|
||||||
|
"naive-ui": "^2.41.0",
|
||||||
"nuxt": "^3.15.0",
|
"nuxt": "^3.15.0",
|
||||||
|
"nuxtjs-naive-ui": "^1.0.2",
|
||||||
"pinia": "^2.3.0",
|
"pinia": "^2.3.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
|
"unplugin-auto-import": "^19.0.0",
|
||||||
|
"unplugin-vue-components": "^28.0.0",
|
||||||
"vue-tsc": "^2.2.0",
|
"vue-tsc": "^2.2.0",
|
||||||
"vueuc": "^0.4.64"
|
"vueuc": "^0.4.64"
|
||||||
},
|
},
|
||||||
|
@ -50,6 +55,8 @@
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"lucide-vue-next": "^0.471.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
295
pnpm-lock.yaml
295
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue