添加 Vue 组件、插件和配置文件,包含用户认证、对话框和样式设置

This commit is contained in:
keqingmoe 2024-12-25 23:39:23 +08:00
parent 58e93e295b
commit 7d94b2fa66
36 changed files with 5618 additions and 21 deletions

View File

@ -1,21 +1,141 @@
#include "server/auth.h"
#include "server/config.h"
#include "server/types.h"
#include "jwt/jwt.h"
#include "db/auth.h"
#include "db/db.h"
#include <civetweb.h> #include <civetweb.h>
#include <kqm/defs.h> #include <signal.h>
#include <kqm/types.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
leveldb_t* db = NULL;
void signal_handler(int signal)
{
fprintf(stderr, "Signal %d received, cleaning up...\n", signal);
close_db(db);
exit(1);
}
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
const char* options[] = {"document_root", "./www", "listening_ports", "8080", NULL}; open_user_db();
// while (1) {
// printf("> ");
// char op[16];
// scanf("%15s", op);
// if (strcmp(op, "exit") == 0) break;
// if (strcmp(op, "login") == 0) {
// char user_id[64];
// char password[64];
// scanf("%63s %63s", user_id, password);
// int result;
// int flag = login(user_id, password, &result);
// if (!flag) {
// printf("Failed to login\n");
// } else {
// printf("Result: %s\n", result ? "login success" : "login failed");
// }
// }else if(strcmp(op, "logout") == 0){
// char user_id[64];
// scanf("%63s", user_id);
// int flag = logout(user_id);
// if (!flag) {
// printf("Failed to logout\n");
// } else {
// printf("Logout success\n");
// }
// } else if (strcmp(op, "register") == 0) {
// char user_id[64];
// char password[64];
// scanf("%63s %63s", user_id, password);
// int result;
// int flag = check_user_exists(user_id, &result);
// if(!flag){
// printf("Failed to check user existence\n");
// continue;
// }else if (result) {
// printf("User already exists\n");
// continue;
// }
// flag = set_user_password(user_id, password);
// if (!flag) {
// printf("Failed to register\n");
// } else {
// printf("Registration success\n");
// }
// } else if (strcmp(op, "check") == 0) {
// char user_id[64];
// scanf("%63s", user_id);
// int result;
// int flag = check_user_exists(user_id, &result);
// if (!flag) {
// printf("Failed to check user existence\n");
// } else {
// printf("Result: %s\n", result ? "user exists" : "user does not exist");
// }
// }
// }
// getchar();
// char* jwt = create_token("Hello", "world");
// printf("JWT: %s\n", jwt);
// int flag = verify_token(jwt, "world");
// printf("Flag: %d\n", flag);
// char* id = get_payload(jwt);
// printf("ID: %s\n", id);
// free(id);
// free(jwt);
// if (!(db = open_db("db/main"))) return 1;
// signal(SIGINT, signal_handler);
// signal(SIGTERM, signal_handler);
// signal(SIGSEGV, signal_handler);
config_t config;
int stat = config_read(&config, "configs/config.json");
if (!stat) {
printf("config_read() returned %d\n", stat);
config_dtor(&config);
return 1;
}
if (config.server_port > 65535 || config.server_port <= 0) {
printf("Invalid server_port: %d\n", config.server_port);
config_dtor(&config);
return 1;
}
secret = config.secret;
char server_port_str[6];
snprintf(server_port_str, sizeof(server_port_str), "%d", config.server_port);
const char* options[] =
{"document_root", "./www", "listening_ports", server_port_str, "error_log_file", "logs/civetweb.log", NULL};
mg_callbacks callbacks; mg_callbacks callbacks;
mg_context* ctx; mg_context* ctx;
memset(&callbacks, 0, sizeof(callbacks)); memset(&callbacks, 0, sizeof(callbacks));
ctx = mg_start(&callbacks, NULL, options); ctx = mg_start(&callbacks, NULL, options);
mg_set_request_handler(ctx, "/api/auth/login", login_handler, NULL);
mg_set_request_handler(ctx, "/api/auth/register", register_handler, NULL);
mg_set_request_handler(ctx, "/api/auth/delete", delete_handler, NULL);
printf("Server started on port(s) %s\n", mg_get_option(ctx, "listening_ports")); printf("Server started on port(s) %s\n", mg_get_option(ctx, "listening_ports"));
getchar(); getchar();
mg_stop(ctx); mg_stop(ctx);
close_user_db();
// close_db(db);
return 0;
} }

View File

@ -136,7 +136,7 @@ int login_handler(mg_connection* conn, void* cbdata)
"HTTP/1.1 200 OK\r\n" "HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n" "Content-Type: application/json\r\n"
"Access-Control-Allow-Origin: *\r\n\r\n" "Access-Control-Allow-Origin: *\r\n\r\n"
"{\"token\":\"%s\"}", "{\"success\":\"login success\", \"token\":\"%s\"}",
token); token);
free(token); free(token);
} }

4
ui/.browserslistrc Normal file
View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

6
ui/.editorconfig Normal file
View File

@ -0,0 +1,6 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

22
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

81
ui/README.md Normal file
View File

@ -0,0 +1,81 @@
# Vuetify (Default)
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
## ❗️ Important Links
- 📄 [Docs](https://vuetifyjs.com/)
- 🚨 [Issues](https://issues.vuetifyjs.com/)
- 🏬 [Store](https://store.vuetifyjs.com/)
- 🎮 [Playground](https://play.vuetifyjs.com/)
- 💬 [Discord](https://community.vuetifyjs.com)
## 💿 Install
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
| Package Manager | Command |
|---------------------------------------------------------------|----------------|
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
| [bun](https://bun.sh/#getting-started) | `bun install` |
After completing the installation, your environment is ready for Vuetify development.
## ✨ Features
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
## 💡 Usage
This section covers how to start the development server and build your project for production.
### Starting the Development Server
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
```bash
yarn dev
```
(Repeat for npm, pnpm, and bun with respective commands.)
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
### Building for Production
To build your project for production, use:
```bash
yarn build
```
(Repeat for npm, pnpm, and bun with respective commands.)
Once the build process is completed, your application will be ready for deployment in a production environment.
## 💪 Support Vuetify Development
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
## 📑 License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2016-present Vuetify, LLC

21
ui/components.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
/* 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 {
Auth: typeof import('./src/components/Auth.vue')['default']
Dialog: typeof import('./src/components/Dialog.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
HelloWorld2: typeof import('./src/components/HelloWorld2.vue')['default']
LoginForm: typeof import('./src/components/LoginForm.vue')['default']
RegisterForm: typeof import('./src/components/RegisterForm.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Toy: typeof import('./src/components/Toy.vue')['default']
UserPanel: typeof import('./src/components/UserPanel.vue')['default']
}
}

2
ui/env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />

36
ui/eslint.config.js Normal file
View File

@ -0,0 +1,36 @@
/**
* .eslint.js
*
* ESLint configuration file.
*/
import pluginVue from 'eslint-plugin-vue'
import vueTsEslintConfig from '@vue/eslint-config-typescript'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
...pluginVue.configs['flat/recommended'],
...vueTsEslintConfig(),
{
rules: {
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
},
],
'vue/multi-word-component-names': 'off',
}
}
]

13
ui/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Vuetify 3</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

4222
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
ui/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "ui",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --fix"
},
"dependencies": {
"@mdi/font": "7.4.47",
"axios": "^1.7.9",
"core-js": "^3.37.1",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"node-forge": "^1.3.1",
"pinia": "^2.3.0",
"roboto-fontface": "*",
"vue": "^3.4.31",
"vuetify": "^3.6.14"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
"@tsconfig/node22": "^22.0.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.9.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0",
"npm-run-all2": "^7.0.1",
"sass": "1.77.8",
"sass-embedded": "^1.77.8",
"typescript": "~5.6.3",
"unplugin-fonts": "^1.1.1",
"unplugin-vue-components": "^0.27.2",
"unplugin-vue-router": "^0.10.0",
"vite": "^5.4.10",
"vite-plugin-vuetify": "^2.0.3",
"vue-router": "^4.4.0",
"vue-tsc": "^2.1.10"
}
}

BIN
ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

44
ui/src/App.vue Normal file
View File

@ -0,0 +1,44 @@
<template>
<v-app>
<v-layout class="rounded rounded-md">
<v-app-bar color="surface-variant" title="Math">
<v-btn icon>
<v-icon>mdi-magnify</v-icon>
</v-btn>
<v-btn icon>
<v-icon>mdi-heart</v-icon>
</v-btn>
<v-btn icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</v-app-bar>
<v-navigation-drawer>
<v-list nav>
<v-list-item to="/" prepend-icon="mdi-email" title="Inbox" value="inbox"></v-list-item>
<v-list-item to="/auth" prepend-icon="mdi-account-supervisor-circle" title="登录" value="supervisors"></v-list-item>
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
<v-list-item prepend-icon="mdi-clock-start" :title="token" value="clockin"></v-list-item>
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
</v-list>
</v-navigation-drawer>
<v-main class="d-flex align-center justify-center" style="min-height: 300px;">
<router-view></router-view>
</v-main>
</v-layout>
<!-- <v-main>
<router-view />
</v-main> -->
</v-app>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useAuthStore } from './store/auth';
const authStore = useAuthStore();
const token = ref(authStore.token);
</script>

BIN
ui/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

6
ui/src/assets/logo.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@ -0,0 +1,40 @@
<template>
<v-carousel v-model="loginMode" v-if="!authStore.isAuthenticated" height="100%" hide-delimiters show-arrows>
<v-carousel-item>
<v-sheet class="v-center" height="100%">
<LoginForm id="aaaaa"></LoginForm>
</v-sheet>
</v-carousel-item>
<v-carousel-item>
<v-sheet class="v-center" height="100%">
<RegisterForm v-model="loginMode"></RegisterForm>
</v-sheet>
</v-carousel-item>
</v-carousel>
<UserInfo v-else></UserInfo>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth';
import LoginForm from './LoginForm.vue';
import { ref } from 'vue';
import { jwtDecode, type JwtPayload } from 'jwt-decode';
import RegisterForm from './RegisterForm.vue';
import UserInfo from './UserPanel.vue';
const loginMode = ref(0);
const authStore = useAuthStore();
const token = ref(authStore.token);
</script>
<style lang="scss" scoped>
.v-center {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
/* 设置容器高度为视口高度 */
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<v-dialog max-width="500">
<template v-slot:activator="{ props: activatorProps }">
<v-btn v-bind="activatorProps" color="surface-variant" text="Open Dialog" variant="flat"></v-btn>
</template>
<template v-slot:default="{ isActive }">
<v-card :title="title">
<v-card-text>
<slot></slot>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text="关闭" @click="isActive.value = false"></v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>
<script setup lang="ts">
const { title = '' } = defineProps<{
title?: string
}>()
</script>

View File

@ -0,0 +1,7 @@
<template>
点左边登录
</template>
<script setup lang="ts">
//
</script>

View File

@ -0,0 +1,157 @@
<template>
<v-container class="fill-height">
<v-responsive
class="align-centerfill-height mx-auto"
max-width="900"
>
<v-img
class="mb-4"
height="150"
src="@/assets/logo.png"
/>
<div class="text-center">
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
<h1 class="text-h2 font-weight-bold">Vuetify 2</h1>
</div>
<div class="py-4"></div>
<v-row>
<v-col cols="12">
<v-card
class="py-4"
color="surface-variant"
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
prepend-icon="mdi-rocket-launch-outline"
rounded="lg"
variant="outlined"
>
<template #image>
<v-img position="top right" />
</template>
<template #title>
<h2 class="text-h5 font-weight-bold">Get started</h2>
</template>
<template #subtitle>
<div class="text-subtitle-1">
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
</div>
</template>
<v-overlay
opacity=".12"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/"
prepend-icon="mdi-text-box-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Learn about all things Vuetify in our documentation."
target="_blank"
title="Documentation"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
prepend-icon="mdi-star-circle-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Explore available framework Features."
target="_blank"
title="Features"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://vuetifyjs.com/components/all"
prepend-icon="mdi-widgets-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Discover components in the API Explorer."
target="_blank"
title="Components"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
<v-col cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
href="https://discord.vuetifyjs.com"
prepend-icon="mdi-account-group-outline"
rel="noopener noreferrer"
rounded="lg"
subtitle="Connect with Vuetify developers."
target="_blank"
title="Community"
variant="text"
>
<v-overlay
opacity=".06"
scrim="primary"
contained
model-value
persistent
/>
</v-card>
</v-col>
</v-row>
</v-responsive>
</v-container>
</template>
<script setup lang="ts">
//
</script>

View File

@ -0,0 +1,157 @@
<!-- <template>
<v-sheet class="mx-auto text-center" min-width="50%">
<v-form fast-fail @submit.prevent="submit">
<v-container>
<v-row>
<v-col>
<h1 class="text-h2 font-weight-bold">登录</h1>
</v-col>
</v-row>
<v-spacer></v-spacer>
<v-row>
<v-col>
<v-text-field v-model="userId" :rules="userIdRules" label="账号"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="password" :rules="passwordRules" :error-messages="passwordValidate"
label="密码"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn :loading="loading" width="50%" text="注册" type="submit"></v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
</v-sheet>
</template> -->
<template>
<div>
<v-form fast-fail @submit.prevent="submit">
<v-img class="mx-auto my-6" max-width="228"
src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img>
<v-card class="mx-auto pa-12 pb-8" elevation="8" max-width="448" rounded="lg">
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
label="账号"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field v-model="password" :rules="passwordRules" :error-messages="passwordValidate" label="密码"
:append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'" :type="visible ? 'text' : 'password'"
@click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-card class="mb-12" color="surface-variant" variant="tonal">
<v-card-text class="text-medium-emphasis text-caption">
警告连续3次登录失败后您的帐户将被暂时锁定三个小时如果您必须立即登录您还可以点击忘记登录密码下面重置登录密码
<br />
这只是个占位符文本不知道为什么没有这段话整个布局就会乱成一坨实际上我并没有做这个冻结功能
</v-card-text>
</v-card>
<v-btn class="mb-8" color="blue" size="large" variant="tonal" type="submit" block>
登录
</v-btn>
<v-card-text class="text-center">
<a class="text-red text-decoration-none" @click="dialog('没做呢。', '没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。')">
忘记密码<v-icon icon="mdi-chevron-right"></v-icon>
</a>
</v-card-text>
</v-card>
</v-form>
</div>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
import { jwtDecode } from 'jwt-decode';
import { ref } from 'vue';
const visible = ref(true);
const authStore = useAuthStore();
const storedToken = ref(authStore.token);
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const loading = ref(false);
const userId = ref('');
const password = ref('');
const passwordValidate = ref<string>("");
type LoginResponse = { success?: string, token?: string, error?: string };
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const login = async (userId: string, password: string) => {
console.log('login', userId, password);
try {
const formData = new FormData;
formData.append("user_id", userId);
formData.append("password", password);
let res = await axios.post('/api/auth/login', formData);
console.log(res.data);
return res.data as LoginResponse;
} catch (e) {
let ex = e as AxiosError;
return ex.response?.data as LoginResponse;
}
};
const submit = async (event: SubmitEvent) => {
const results: any = await event;
if (results.valid) {
loading.value = true
console.log('valid')
let res = await login(userId.value, password.value);
if (res?.error) {
passwordValidate.value = res.error;
await dialog('错误', `登录失败:${res.error}`);
} else {
await dialog('信息', `登录成功,你好 ${(jwtDecode(res.token as string) as any).user_id}`);
authStore.setToken(res.token as string);
}
loading.value = false
}
console.log(2);
};
const userIdRules: any = [(value: string) => {
passwordValidate.value = '';
if (value?.length > 0) return true;
return '账号不能为空';
}];
const passwordRules: any = [(value: string) => {
passwordValidate.value = '';
if (value?.length > 0) return true;
return '密码不能为空';
}];
</script>

View File

@ -0,0 +1,125 @@
<template>
<div>
<v-form fast-fail @submit.prevent="submit">
<v-img class="mx-auto my-6" max-width="228"
src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img>
<v-card class="mx-auto pa-12 pb-8" elevation="8" max-width="448" rounded="lg">
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
label="账号"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field v-model="password" :rules="passwordRules" label="密码"
:append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'" :type="visible ? 'text' : 'password'"
@click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field :rules="passwordRules2" label="重复输入密码" :append-inner-icon="visible ? 'mdi-eye-off' : 'mdi-eye'"
:type="visible ? 'text' : 'password'" @click:append-inner="visible = !visible"
prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-card class="mb-12" color="surface-variant" variant="tonal">
<v-card-text class="text-medium-emphasis text-caption">
这只是个占位符文本不知道为什么没有这段话整个布局就会乱成一坨我还在研究怎么让它表现得正常一点这个组件库的文档太抽象了看不懂
</v-card-text>
</v-card>
<v-btn :loading="loading" class="mb-8" color="blue" size="large" type="submit" variant="tonal" block>
注册
</v-btn>
</v-card>
</v-form>
</div>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
import { inject, ref } from 'vue';
const visible = ref(true);
const model = defineModel();
const authStore = useAuthStore();
const storedToken = ref(authStore.token);
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const loading = ref(false);
const userId = ref('');
const password = ref('');
type RegisterResponse = { success?: string, error?: string };
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const register = async (userId: string, password: string) => {
console.log('login', userId, password);
try {
const formData = new FormData;
formData.append("user_id", userId);
formData.append("password", password);
let res = await axios.post('/api/auth/register', formData);
console.log(res.data);
return res.data as RegisterResponse;
} catch (e) {
let ex = e as AxiosError;
return ex.response?.data as RegisterResponse;
}
};
const submit = async (event: SubmitEvent) => {
const results: any = await event;
if (results.valid) {
loading.value = true
console.log('valid')
let res = await register(userId.value, password.value);
if (res?.error) {
dialog('错误', `注册失败:${res.error}`);
} else {
await dialog('提示', '注册成功,请前往登录。');
model.value = 0;
}
loading.value = false
}
console.log(2);
};
const userIdRules: any = [(value: string) => {
if (value?.length > 0) return true;
return '账号不能为空';
}];
const passwordRules: any = [(value: string) => {
if (value?.length > 0) return true;
return '密码不能为空';
}];
const passwordRules2: any = [(value: string) => {
if (value == password.value) return true;
return '两次输入需保持一致';
}];
</script>

80
ui/src/components/Toy.vue Normal file
View File

@ -0,0 +1,80 @@
<template>
<v-row align="center" justify="center" dense>
<v-col cols="12" md="6">
<v-card append-icon="mdi-check" class="mx-auto" prepend-icon="mdi-account" subtitle="占位符。占位符。" title="占位符">
<v-card-text>占位符占位符占位符占位符</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card class="mx-auto" subtitle="占位符。占位符。" title="占位符">
<template v-slot:prepend>
<v-icon color="primary" icon="mdi-account"></v-icon>
</template>
<template v-slot:append>
<v-icon color="success" icon="mdi-check"></v-icon>
</template>
<v-card-text>占位符占位符占位符占位符</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card append-avatar="https://cdn.vuetifyjs.com/images/john.jpg" class="mx-auto"
prepend-avatar="https://cdn.vuetifyjs.com/images/logos/v-alt.svg" subtitle="占位符。占位符。" title="占位符">
<v-card-text>占位符占位符占位符占位符</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card class="mx-auto" subtitle="占位符。占位符。" title="占位符">
<template v-slot:prepend>
<v-avatar color="blue-darken-2">
<v-icon icon="mdi-alarm"></v-icon>
</v-avatar>
</template>
<template v-slot:append>
<v-avatar size="24">
<v-img alt="John" src="https://file.aiquickdraw.com/inpaint/1735136797_9819bf1a-7f1d-4a32-be6a-f5f7f3977b71.png"></v-img>
</v-avatar>
</template>
<v-card-text>占位符占位符占位符占位符</v-card-text>
</v-card>
</v-col>
</v-row>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const labels = ref([
'12am',
'3am',
'6am',
'9am',
'12pm',
'3pm',
'6pm',
'9pm',
])
const value = ref([
200,
675,
410,
390,
310,
460,
250,
240,
]);
</script>
<style scoped>
.v-sheet--offset {
top: -24px;
position: relative;
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<v-card subtitle="占位符占位符占位符" title="用户信息面板" max-width="80%">
<v-card-item>
用户 ID: {{ userId }}
</v-card-item>
<v-card-item>
<v-chip @click="authStore.clearToken">
退出登录
</v-chip>
</v-card-item>
<v-card-item>
<v-expansion-panels>
<v-expansion-panel title="Token (放出来玩玩,之后会换个机制)" :text="token">
</v-expansion-panel>
</v-expansion-panels>
</v-card-item>
<v-card-item>
<Toy></Toy>
</v-card-item>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useAuthStore } from '@/store/auth';
import { jwtDecode, type JwtPayload } from 'jwt-decode';
import Toy from './Toy.vue';
const reveal = ref(false);
interface KqmJwt extends JwtPayload {
user_id: string;
};
const authStore = useAuthStore();
const token = ref(authStore.token);
const userId = computed(() => {
if (authStore.isAuthenticated) {
let data: KqmJwt = jwtDecode(token.value);
return data.user_id;
}
return '';
})
</script>

25
ui/src/main.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* main.ts
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
import { createPinia } from 'pinia';
const app = createApp(App)
const pinia = createPinia();
app.use(pinia);
registerPlugins(app)
app.mount('#app')

18
ui/src/plugins/index.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* plugins/index.ts
*
* Automatically included in `./src/main.ts`
*/
// Plugins
import vuetify from './vuetify'
import router from '../router'
// Types
import type { App } from 'vue'
export function registerPlugins (app: App) {
app
.use(vuetify)
.use(router)
}

19
ui/src/plugins/vuetify.ts Normal file
View File

@ -0,0 +1,19 @@
/**
* plugins/vuetify.ts
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
defaultTheme: 'light',
},
})

66
ui/src/router/index.ts Normal file
View File

@ -0,0 +1,66 @@
/**
* router/index.ts
*
* Automatic routes for `./src/pages/*.vue`
*/
// Composables
import Auth from '@/components/Auth.vue'
import HelloWorld from '@/components/HelloWorld.vue'
import HelloWorld2 from '@/components/HelloWorld2.vue'
import Login from '@/components/RegisterForm.vue'
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'home',
component: HelloWorld
},
{
path: '/auth',
name: 'home2',
component: Auth
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// const routes = [
// {
// path: '/',
// name: 'home',
// componen
// }
// ]
// const router = createRouter({
// history: createWebHistory(import.meta.env.BASE_URL),
// routes,
// })
// Workaround for https://github.com/vitejs/vite/issues/11804
router.onError((err, to) => {
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
if (!localStorage.getItem('vuetify:dynamic-reload')) {
console.log('Reloading page to fix dynamic import error')
localStorage.setItem('vuetify:dynamic-reload', 'true')
location.assign(to.fullPath)
} else {
console.error('Dynamic import error, reloading page did not fix it', err)
}
} else {
console.error(err)
}
})
router.isReady().then(() => {
localStorage.removeItem('vuetify:dynamic-reload')
})
export default router

20
ui/src/store/auth.ts Normal file
View File

@ -0,0 +1,20 @@
import { defineStore } from "pinia";
export const useAuthStore = defineStore("auth", {
state: () => ({
token: localStorage.getItem('token') || '',
}),
actions: {
setToken(token: string) {
this.token = token;
localStorage.setItem('token', token);
},
clearToken() {
this.token = '';
localStorage.removeItem('token');
},
},
getters: {
isAuthenticated: (state) => !!state.token,
},
});

View File

@ -0,0 +1,10 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $color-pack: false
// );

14
ui/tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

14
ui/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
"compilerOptions": {
"module": "es2020",
}
}

19
ui/tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

22
ui/typed-router.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
}
}

61
ui/vite.config.mts Normal file
View File

@ -0,0 +1,61 @@
// Plugins
import Components from 'unplugin-vue-components/vite'
import Vue from '@vitejs/plugin-vue'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import ViteFonts from 'unplugin-fonts/vite'
import VueRouter from 'unplugin-vue-router/vite'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter(),
Vue({
template: { transformAssetUrls },
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
},
}),
Components(),
ViteFonts({
google: {
families: [ {
name: 'Roboto',
styles: 'wght@100;300;400;500;700;900',
}],
},
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
css: {
preprocessorOptions: {
sass: {
api: 'modern-compiler',
},
},
},
})

View File

@ -1,27 +1,70 @@
add_rules("mode.debug", "mode.release") add_rules("mode.debug", "mode.release")
add_rules("plugin.compile_commands.autoupdate") add_rules("plugin.compile_commands.autoupdate")
set_languages("c23")
set_warnings("all") set_warnings("all")
set_warnings("error") set_warnings("error")
add_requires("civetweb") add_requires("civetweb", "cjson", "leveldb", "jwt-cpp", "cryptopp")
target("math")
set_kind("binary")
add_packages("civetweb")
add_includedirs("include")
add_files("src/main.c")
after_build(function (target)
local npm = "npm" local npm = "npm"
if is_host("windows") then if is_host("windows") then
npm = "npm.cmd" npm = "npm.cmd"
end end
os.execv(npm, {"--prefix", "ui", "run", "build"})
local vue_dist_dir = "ui/dist" target("jwt")
local target_dir = target:targetdir() .. "/www" set_languages("c++23")
os.cp(vue_dist_dir, target_dir) set_kind("static")
add_packages("jwt-cpp")
add_includedirs("include/jwt")
add_files("src/jwt/**.cpp")
target("hash")
set_languages("c++23")
set_kind("static")
add_packages("cryptopp")
add_includedirs("include/hash")
add_files("src/hash/**.cpp")
target("db")
set_languages("c++23")
set_kind("static")
add_packages("leveldb")
add_deps("hash")
add_includedirs("include/db", "include")
add_files("src/db/**.cpp")
after_build(function (target)
local target_dir = target:targetdir()
os.cp("configs", target_dir)
if not os.exists(target_dir .. "/db") then
os.mkdir(target_dir .. "/db")
end
end)
target("server")
set_languages("c23")
set_kind("binary")
add_packages("civetweb", "cjson")
add_deps("jwt", "db")
add_includedirs("include")
add_files("src/server/**.c")
add_files("src/main.c")
target("ui")
on_build(function (target)
os.execv(npm, {"--prefix", "ui", "run", "build"})
local target_dir = target:targetdir()
os.cp("ui/dist", target_dir)
os.mv(target_dir .. "/dist", target_dir .. "/www")
end)
on_run(function (target)
os.execv(npm, {"--prefix", "ui", "run", "dev"})
end)
target("math")
add_deps("ui")
add_deps("server")
on_build(function (target) end)
on_run(function (target)
os.exec("xmake run server")
end) end)