提交代码

This commit is contained in:
dandan 2024-10-28 23:43:18 +08:00
parent 4c8b0ca7e2
commit 66d4850ad5
21 changed files with 683 additions and 4666 deletions

View File

@ -29,6 +29,9 @@ type Service interface {
// GetUser 获取用户信息
GetUser(ctx context.Context, userID int64) (*User, error)
// EditUser 编辑用户信息
EditUser(ctx context.Context, user *User) (*User, error)
}
func RegisterType(typeRegister contract.TypeRegisterService) {
@ -38,14 +41,26 @@ func RegisterType(typeRegister contract.TypeRegisterService) {
// User 代表一个用户,注意这里的用户信息字段在不同接口和参数可能为空
type User struct {
ID int64 `gorm:"column:id;primary_key;auto_increment" json:"id"` // 代表用户id, 只有注册成功之后才有这个id唯一表示一个用户
UserName string `gorm:"column:username;type:varchar(255);not null" json:"username"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"password"`
Email string `gorm:"column:email;type:varchar(255);not null" json:"email"`
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;<-:create" json:"createdAt"`
UserName string `gorm:"column:username;type:varchar(255);comment:用户名;not null" json:"username"`
NickName string `gorm:"column:username;type:varchar(255);comment:昵称;not null" json:"nickname"`
Avatar string `gorm:"column:username;type:varchar(255);comment:头像" json:"avatar"`
Password string `gorm:"column:password;type:varchar(255);comment:密码;not null" json:"password"`
Email string `gorm:"column:email;type:varchar(255);comment:邮箱;not null" json:"email"`
CreatedAt time.Time `gorm:"column:created_at;type:datetime;comment:创建时间;not null;<-:create" json:"createdAt"`
Accounts []Account `gorm:"foreignKey:UserID"`
Token string `gorm:"-"` // token 可以用作注册token或者登录token
}
// Account 代表一个用户账户信息,有可能有多种登录方式
type Account struct {
ID int64 `gorm:"column:id;primary_key;auto_increment" json:"id"`
UserID int64 `gorm:"column:user_id;index;comment:用户ID;not null;default:0" json:"userId"`
AccountType int64 `gorm:"column:account;type:varchar(255);comment:账户类型;not null;default:0" json:"account"`
Password string `gorm:"column:password;type:varchar(255);comment:密码;not null" json:"password"`
Remark string `gorm:"column:remark;type:varchar(255);comment:备注;not null" json:"remark"`
CreatedAt time.Time `gorm:"column:created_at;type:datetime;comment:创建时间;not null" json:"createdAt"`
}
// MarshalBinary 实现BinaryMarshaler 接口
func (b *User) MarshalBinary() ([]byte, error) {
return json.Marshal(b)

4611
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,14 +10,17 @@
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"element-plus": "^2.6.2",
"vue": "^3.4.21"
"element-plus": "^2.8.6",
"js-cookie": "^3.0.5",
"vue": "^3.4.21",
"vue-router": "^4.4.5",
"vuex": "^4.0.2"
},
"devDependencies": {
"@iconify-json/ep": "^1.1.15",
"@types/node": "^20.11.30",
"@vitejs/plugin-vue": "^5.0.4",
"sass": "^1.72.0",
"sass": "^1.56.0",
"typescript": "^5.4.3",
"unocss": "^0.58.6",
"unplugin-vue-components": "^0.26.0",

View File

@ -1,23 +1,17 @@
<template>
<el-config-provider namespace="ep">
<BaseHeader />
<div class="flex main-container">
<BaseSide />
<div w="full" py="4">
<Logos my="4" />
<HelloWorld msg="Hello Vue 3 + Element Plus + Vite" />
</div>
<!-- 使用 router-view 渲染匹配的路由组件 -->
<router-view />
</div>
</el-config-provider>
</template>
<style>
#app {
text-align: center;
color: var(--ep-text-color-primary);
}
.main-container {
height: calc(100vh - var(--ep-menu-item-height) - 3px);
}
</style>
<script setup lang="ts">
</script>

8
src/components.d.ts vendored
View File

@ -10,17 +10,25 @@ declare module 'vue' {
BaseHeader: typeof import('./components/layouts/BaseHeader.vue')['default']
BaseSide: typeof import('./components/layouts/BaseSide.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTag: typeof import('element-plus/es')['ElTag']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
Logos: typeof import('./components/Logos.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View File

@ -1,31 +1,71 @@
<script lang="ts" setup>
import { toggleDark } from "~/composables";
import {toggleDark} from '~/composables';
import {computed, PropType, ref} from 'vue';
import {useStore} from 'vuex';
// 使 Vuex store
const store = useStore();
//
const isLoggedIn = computed(() => store.state.user.isLoggedIn);
//
const activeIndex = ref('1');
</script>
<template>
<el-menu class="el-menu-demo" mode="horizontal">
<el-menu-item index="1">Element Plus</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>Info</el-menu-item>
<el-menu-item index="4">Orders</el-menu-item>
<el-menu-item h="full" @click="toggleDark()">
<button
class="border-none w-full bg-transparent cursor-pointer"
style="height: var(--ep-menu-item-height)"
<el-menu
:default-active="activeIndex"
class="el-menu-demo"
:ellipsis="false"
mode="horizontal"
>
<i inline-flex i="dark:ep-moon ep-sunny" />
<div class="menu-left">
<!-- 渲染左侧菜单项 -->
<el-menu-item index="0">Processing Center</el-menu-item>
<el-menu-item v-if="isLoggedIn" index="1">Dashboard</el-menu-item>
<el-menu-item v-if="isLoggedIn" index="2">Orders</el-menu-item>
</div>
<!-- 右侧按钮 -->
<div class="menu-right">
<el-menu-item index="3" @click="toggleDark()">
<button
class="border-none w-full bg-transparent cursor-pointer">
<i inline-flex i="dark:ep-moon ep-sunny"/>
</button>
</el-menu-item>
</div>
</el-menu>
</template>
<style scoped>
.el-menu-demo {
display: flex; /* 使用 Flex 布局 */
justify-content: space-between; /* 左右对齐 */
align-items: center; /* 垂直居中 */
width: 100%;
padding: 0 16px; /* 添加一些内边距 */
box-sizing: border-box;
}
.menu-left {
display: flex; /* 左侧菜单项的布局 */
gap: 16px; /* 菜单项之间的间距 */
}
.menu-right {
display: flex; /* 右侧按钮的布局 */
align-items: center;
}
.toggle-dark-btn {
border: none;
background-color: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
}
</style>

View File

@ -11,10 +11,13 @@ import App from "./App.vue";
import "~/styles/index.scss";
import "uno.css";
import router from "./router/index"; // 导入 router
import store from './store/index'
// If you want to use ElMessage, import it.
import "element-plus/theme-chalk/src/message.scss";
const app = createApp(App);
// app.use(ElementPlus);
app.use(router)
app.use(store)
app.mount("#app");

76
src/router/index.ts Normal file
View File

@ -0,0 +1,76 @@
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import ViewLogin from '../views/login/index.vue';
import ViewRegister from '../views/register/index.vue';
import View404 from '../views/404.vue';
import ViewContainer from '../views/layout/container.vue';
import ViewList from '../views/list/index.vue';
import ViewDetail from '../views/detail/index.vue';
import ViewCreate from '../views/create/index.vue';
import ViewEdit from '../views/edit/index.vue';
/**
* constantRoutes
*
* 访
*/
export const constantRoutes: Array<RouteRecordRaw> = [
{
path: '/login',
component: ViewLogin,
meta: { hidden: true },
},
{
path: '/register',
component: ViewRegister,
meta: { hidden: true },
},
{
path: '/404',
component: View404,
meta: { hidden: true },
},
{
path: '/',
component: ViewContainer,
redirect: '/list',
children: [
{
path: 'list',
name: 'List',
component: ViewList,
},
{
path: 'detail',
name: 'Detail',
component: ViewDetail,
},
{
path: 'create',
name: 'Create',
component: ViewCreate,
},
{
path: 'edit',
name: 'Edit',
component: ViewEdit,
},
],
},
// 404 页面必须放在最后
{ path: '/:pathMatch(.*)*', redirect: '/404', meta: { hidden: true } },
];
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes,
});
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router;

View File

@ -1,5 +1,26 @@
import {
AnswerCreateParam,
AnswerDeleteParam,
LoginParam,
QuestionCreateParam,
QuestionDeleteParam,
QuestionDetailParam,
QuestionEditParam,
QuestionListParam,
RegisterParam
} from "~/services/apiTypes"
import {ElMessage} from "element-plus";
// 定义基础 URL
const BASE_URL = '/';
const BASE_URL = 'http://127.0.0.1:8888';
// 定义后端返回的数据结构类型
interface ApiResponse<T> {
code: number;
success: boolean;
message: string;
data?: T;
}
// 通用的 fetch 请求封装函数
async function request<T>(
@ -16,15 +37,19 @@ async function request<T>(
},
body: body ? JSON.stringify(body) : undefined,
};
console.log(options)
try {
const response = await fetch(`${BASE_URL}${endpoint}`, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API Error: ${response.status} ${errorText}`);
const result: ApiResponse<T> = await response.json();
if (!result.success) {
ElMessage.error(result.message)
}
// 返回成功的数据
return result.data as T;
} catch (error) {
console.error('请求出错:', error);
ElMessage.error("网络开小差")
}
return response.json();
}
// 用户相关 API 封装

15
src/store/getters.ts Normal file
View File

@ -0,0 +1,15 @@
import { GetterTree } from 'vuex';
import { RootState } from './index';
const getters: GetterTree<RootState, RootState> = {
sidebar: (state) => state.app.sidebar,
device: (state) => state.app.device,
size: (state) => state.app.size,
token: (state) => state.user.token,
avatar: (state) => state.user.avatar,
name: (state) => state.user.name,
visitedViews: (state) => state.tagsView.visitedViews,
cachedViews: (state) => state.tagsView.cachedViews,
};
export default getters;

28
src/store/index.ts Normal file
View File

@ -0,0 +1,28 @@
// src/store/index.ts
import { createStore, Store, ModuleTree } from 'vuex';
import getters from './getters';
// 定义 RootState 的类型
export interface RootState {
[key: string]: any;
}
// 自动导入 `modules` 文件夹中的 Vuex 模块
const modulesFiles = import.meta.glob('./modules/**/*.ts', { eager: true });
// 遍历所有模块,并动态注册
const modules: ModuleTree<RootState> = Object.keys(modulesFiles).reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/modules\/(.*)\.\w+$/, '$1');
const value = modulesFiles[modulePath] as { default: any };
modules[moduleName] = value.default;
return modules;
}, {} as ModuleTree<RootState>);
// 创建 Vuex Store
const store: Store<RootState> = createStore({
modules,
getters,
});
export default store;

93
src/store/modules/user.ts Normal file
View File

@ -0,0 +1,93 @@
// src/store/modules/user.ts
import {Module} from 'vuex';
import {getToken, removeToken, setToken} from '~/utils/auth';
import {resetRouter} from '~/router';
import {userService} from '~/services/apiServices';
// 定义 UserState 的类型
export interface UserState {
token: string | null;
name: string;
avatar: string;
isLoggedIn: boolean; // 是否已登录
}
// 获取默认状态
const getDefaultState = (): UserState => ({
token: getToken(),
name: 'Superdandan',
avatar: 'https://www.bing.com/images/search?view=detailV2&ccid=UyaBji0A&id=215CD76D0E1089B1CC80B1DC80500B19262DC18C&thid=OIP.UyaBji0AU_6M3VDA2F1RvgAAAA&mediaurl=https%3a%2f%2fgd-hbimg.huaban.com%2fe7b770bf874c9ae0a90976608d0ea889b889d4017ed22-0hmCwW_fw236&cdnurl=https%3a%2f%2fth.bing.com%2fth%2fid%2fR.5326818e2d0053fe8cdd50c0d85d51be%3frik%3djMEtJhkLUIDcsQ%26pid%3dImgRaw%26r%3d0&exph=236&expw=236&q=%e5%a4%b4%e5%83%8f&simid=608001262209275221&FORM=IRPRST&ck=D7DEB083F3CBBF51E0A49A21A7E0E213&selectedIndex=27&itb=1',
isLoggedIn: !!getToken(), // 根据 token 判断是否已登录
});
// 初始状态
const state: UserState = getDefaultState();
// Mutations
const mutations = {
RESET_STATE(state: UserState) {
Object.assign(state, getDefaultState());
state.isLoggedIn = false
},
SET_TOKEN(state: UserState, token: string) {
state.token = token;
state.isLoggedIn = true
},
SET_NAME(state: UserState, name: string) {
state.name = name;
},
SET_AVATAR(state: UserState, avatar: string) {
state.avatar = avatar;
},
};
// Actions
const actions = {
// 用户登录
async login({commit}: any, userInfo: { username: string; password: string }) {
const {username, password} = userInfo;
try {
const response = await userService.login({
username: username.trim(),
password,
});
const token = response.data;
commit('SET_TOKEN', token);
setToken(token);
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
},
// 用户登出
async logout({commit, state}: any) {
try {
await userService.logout();
removeToken(); // 必须先移除 token
resetRouter();
commit('RESET_STATE');
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
},
// 重置 Token
async resetToken({commit}: any) {
removeToken(); // 必须先移除 token
commit('RESET_STATE');
return Promise.resolve();
},
};
// 定义模块
const user: Module<UserState, any> = {
namespaced: true,
state,
mutations,
actions,
};
export default user;

31
src/utils/auth.ts Normal file
View File

@ -0,0 +1,31 @@
// src/utils/auth.ts
import Cookies from 'js-cookie';
// 定义 Token 在 Cookie 中的键名
const TokenKey = 'hade_bbs';
/**
* Token
* @returns {string | undefined} Token undefined
*/
export function getToken(): string | undefined {
return Cookies.get(TokenKey);
}
/**
* Token
* @param token Token
* @returns {void}
*/
export function setToken(token: string): void {
Cookies.set(TokenKey, token, { expires: 7 }); // 过期时间 7 天
}
/**
* Token
* @returns {void}
*/
export function removeToken(): void {
Cookies.remove(TokenKey);
}

31
src/views/404.vue Normal file
View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
// 使 reactive model
const model = reactive({
username: '',
password: ''
});
// loading 使 ref
const loading = ref(false);
</script>
<template>
<div class="notfound">
<el-card>
<h2>页面找不到了</h2>
</el-card>
</div>
</template>
<style scoped>
.notfound {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin-top: 240px;
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

11
src/views/edit/index.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<router-view />
</template>
<style scoped>
</style>

11
src/views/list/index.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

115
src/views/login/index.vue Normal file
View File

@ -0,0 +1,115 @@
<script setup lang="ts">
import {reactive} from 'vue';
import {useRouter} from 'vue-router';
import {userService} from "~/services/apiServices";
import store from "~/store";
const router = useRouter();
const form = reactive({
username: '',
password: '',
});
const handleLogin = () => {
userService.login({
username: form.username,
password: form.password
})
.then((response) => {
// Token Vuex Token
const token = response;
console.log("收到 token" + token)
if (token == undefined) {
return
}
store.commit('user/SET_TOKEN', token);
store.commit('user/SET_NAME', token);
store.commit('user/SET_AVATAR', token);
// token Vuex
const storedToken = store.state.user.token;
if (storedToken === token) {
console.log('Token 已成功存储在 Vuex 中:', storedToken);
} else {
console.error('Token 存储失败');
}
router.push('/');
}).catch((error) => {
console.error('登录错误:', error);
})
};
</script>
<template>
<div class="login">
<el-card>
<h2>登录</h2>
<el-form
class="login-form"
>
<el-form-item prop="username">
<el-input v-model="form.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
placeholder="密码"
type="password"
v-model="form.password"
></el-input>
</el-form-item>
<el-row>
<el-col class="register">
还没有账号请点击
<router-link class="to-link" :to="{path: '/register'}">
<el-link type="primary">注册</el-link>
</router-link>
</el-col>
</el-row>
<el-form-item>
<el-button
class="login-button"
type="primary"
@click="handleLogin"
block
>登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style scoped>
.to-link {
text-decoration: none;
}
.register {
font-weight: 500;
font-size: 14px;
}
.login {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.login-button {
width: 100%;
margin-top: 40px;
}
.login-form {
width: 390px;
}
.forgot-password {
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="register">
<el-card>
<h2>注册</h2>
<el-form :model="form" class="register-form">
<el-form-item>
<el-input v-model="form.username" placeholder="用户名" />
</el-form-item>
<el-form-item>
<el-input v-model="form.email" placeholder="邮箱" />
</el-form-item>
<el-form-item>
<el-input
type="password"
v-model="form.password"
placeholder="密码"
/>
</el-form-item>
<el-form-item>
<el-input
type="password"
v-model="form.repassword"
placeholder="确认密码"
/>
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
class="login-button"
type="primary"
@click="submitForm"
block
>
注册
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { userService } from '~/services/apiServices'; //
const router = useRouter(); //
//
const form = reactive({
username: '',
email: '',
password: '',
repassword: '',
});
//
const loading = ref(false);
//
const submitForm = async () => {
if (form.password !== form.repassword) {
ElMessage.error('两次输入密码不一致');
return;
}
loading.value = true; //
try {
const response = await userService.register(form)
ElMessage.success(response.message); //
//
await router.push('/login');
} finally {
loading.value = false; //
}
};
</script>
<style scoped>
.register {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.login-button {
width: 100%;
margin-top: 40px;
}
.register-form {
width: 390px;
}
</style>