Compare commits

...

No commits in common. 'frontend' and 'main' have entirely different histories.

@ -1,6 +0,0 @@
[*.{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

@ -1,32 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.vscode

@ -1,7 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

@ -1,45 +0,0 @@
# MuseumV2_Frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

@ -1,27 +0,0 @@
// src/api/api.js
import axios from 'axios';
const API_URL = 'http://localhost:8080/api/users';
export const registerUser = async (user) => {
try {
const response = await axios.post(`${API_URL}/register`, user);
return response.data;
} catch (error) {
throw new Error(error.response?.data || 'Registration failed');
}
};
export const loginUser = async (loginAccount, password) => {
try {
const params = new URLSearchParams({
loginAccount,
password
});
const response = await axios.post(`${API_URL}/login`, params);
return response.data;
} catch (error) {
throw new Error(error.response?.data || 'Login failed');
}
};

@ -1,56 +0,0 @@
import axios from 'axios';
// 假设你的后端 API 地址是 http://localhost:8080
const API_URL = 'http://localhost:8080/api/articles';
export default {
// 获取所有文章
getAllArticles() {
return axios.get(`${API_URL}`);
},
// 获取最新文章
getLatestArticles() {
return axios.get(`${API_URL}/latest`);
},
// 获取热门文章
getPopularArticles() {
return axios.get(`${API_URL}/popular`);
},
// 获取推荐文章
getRecommendedArticles() {
return axios.get(`${API_URL}/recommended`);
},
// 获取当前用户的文章列表
getArticlesByCurrentUser() {
return axios.get(`${API_URL}/user/me`);
},
// 创建新文章
createArticle(article) {
return axios.post(`${API_URL}/create`, article);
},
// 获取所有未批准的文章(仅限管理员)
getUnapprovedArticles() {
return axios.get(`${API_URL}/unapproved`);
},
// 审批文章(仅限管理员)
approveArticle(id) {
return axios.put(`${API_URL}/approve/${id}`);
},
// 拒绝文章(仅限管理员)
rejectArticle(id) {
return axios.put(`${API_URL}/reject/${id}`);
},
// 删除文章(仅限管理员)
deleteArticle(id) {
return axios.delete(`${API_URL}/${id}`);
}
};

@ -1,43 +0,0 @@
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder; // 用于加密密码
@Autowired
private JwtUtils jwtUtils; // 用于生成 JWT Token
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials) {
String loginAccount = credentials.get("loginAccount");
String password = credentials.get("password");
// 查找用户
User user = userRepository.findByLoginAccount(loginAccount)
.orElse(null);
if (user != null && passwordEncoder.matches(password, user.getPassword())) {
// 登录成功,生成 JWT Token
String token = jwtUtils.generateToken(user);
// 返回用户信息和 Token
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("user", user);
response.put("token", token);
return ResponseEntity.ok(response);
} else {
// 登录失败
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "账号或密码错误");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}
}
}

@ -1,73 +0,0 @@
import axios from 'axios';
// 使用 import.meta.env 访问环境变量
const apiBaseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api';
// 创建一个 Axios 实例,设置默认配置
const apiClient = axios.create({
baseURL: apiBaseURL,
withCredentials: true, // 确保携带凭证
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
});
export const registerUser = async (user) => {
try {
const response = await apiClient.post('/users/register', user);
return response.data;
} catch (error) {
throw new Error(error.response?.data || error.message);
}
};
export const loginUser = async (loginAccount, password) => {
try {
const params = new URLSearchParams({
loginAccount,
password
});
const response = await apiClient.post('/users/login', params);
return response.data;
} catch (error) {
console.error('Login failed:', error.response?.data || error.message);
throw new Error('Login failed');
}
};
export const getCurrentUser = async () => {
try {
const response = await apiClient.get('/users/me');
return response.data;
} catch (error) {
throw new Error(error.response?.data || error.message);
}
};
export const logoutUser = async () => {
try {
const response = await apiClient.post('/users/logout');
return response.data;
} catch (error) {
throw new Error(error.response?.data || error.message);
}
};
export const updateUser = async (updatedUser) => {
try {
const response = await apiClient.put('/users/me', updatedUser);
return response.data;
} catch (error) {
throw new Error(error.response?.data || error.message);
}
};
export const resetPassword = async (currentPassword, newPassword) => {
try {
const response = await apiClient.post('/users/me/reset-password', { currentPassword, newPassword });
return response.data;
} catch (error) {
throw new Error(error.response?.data || error.message);
}
};

@ -1,11 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

@ -1,52 +0,0 @@
/* 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 {
ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDiver: typeof import('element-plus/es')['ElDiver']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDriver: typeof import('element-plus/es')['ElDriver']
ElFooter: typeof import('element-plus/es')['ElFooter']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElList: typeof import('element-plus/es')['ElList']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
}
}

1
Frontend/env.d.ts vendored

@ -1 +0,0 @@
/// <reference types="vite/client" />

2
Frontend/env/.env vendored

@ -1,2 +0,0 @@
# .env
VITE_SERVER_URL=http://localhost:8080/api/users

@ -1,3 +0,0 @@
# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_SERVER_URL=http://localhost:8080/api/users

@ -1,3 +0,0 @@
VITE_SERVER_URL = "http://127.0.0.1:9099"
VITE_BASE_URL = "/api/v1"
VITE_HTTP_TIMEOUT = 5000

@ -1,25 +0,0 @@
import pluginVue from 'eslint-plugin-vue'
import vueTsEslintConfig from '@vue/eslint-config-typescript'
import pluginVitest from '@vitest/eslint-plugin'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
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/essential'],
...vueTsEslintConfig(),
{
...pluginVitest.configs.recommended,
files: ['src/**/__tests__/*'],
},
skipFormatting,
]

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

File diff suppressed because it is too large Load Diff

@ -1,53 +0,0 @@
{
"name": "museumv2-frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --fix",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.7.9",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.7",
"element-plus": "^2.9.1",
"jsonwebtoken": "^9.0.2",
"museumv2-frontend": "file:",
"pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.1.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.9.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"@vitest/eslint-plugin": "1.1.7",
"@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0",
"jsdom": "^25.0.1",
"npm-run-all2": "^7.0.1",
"prettier": "^3.3.3",
"typescript": "~5.6.3",
"unplugin-auto-import": "^0.18.4",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.10",
"vite-plugin-env-compatible": "^2.0.1",
"vite-plugin-vue-devtools": "^7.5.4",
"vitest": "^2.1.4",
"vue-tsc": "^2.1.10"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

@ -1,11 +0,0 @@
<script setup lang="ts">
// import HomeView from './views/HomeView.vue';
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>

@ -1,24 +0,0 @@
{
"admins": [
{
"id": "1",
"adminname": "admin",
"password": "123456",
"phone": "18963554712",
"signature": "我是管理员",
"birthday": "2024-12-16T16:00:00.000Z",
"security_questions": [
{
"question": "您最新喜欢的歌曲是什么?",
"question_key": "favorite_song",
"answer": "星河叹"
},
{
"question": "您最喜欢的动物是什么?",
"question_key": "favorite_animal",
"answer": "猫猫"
}
]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

@ -1,13 +0,0 @@
/* @import './base.css'; */
body {
margin: 0;
padding: 0;
}
html,body,#app{
margin: 0;
padding: 0;
height: 100%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

@ -1,90 +0,0 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a>
and follow the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

@ -1,87 +0,0 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

@ -1,11 +0,0 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

@ -1,19 +0,0 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

@ -1,44 +0,0 @@
{
"users": [
{
"id": "1",
"username": "黄",
"password": "123456",
"phone": "18963554712",
"signature": "希君生羽翼,一化北溟鱼",
"birthday": "2024-12-16T16:00:00.000Z",
"security_questions": [
{
"question": "您最新喜欢的歌曲是什么?",
"question_key": "favorite_song",
"answer": "星河叹"
},
{
"question": "您最喜欢的动物是什么?",
"question_key": "favorite_animal",
"answer": "猫猫"
}
]
},
{
"id": "2",
"username": "user1",
"password": "123456",
"phone": "15608674327",
"signature": "",
"birthday": null,
"security_questions": [
{
"question": "您小学的名字是什么?",
"question_key": "elementary_school",
"answer": "1"
},
{
"question": "您最喜欢的城市是哪里?",
"question_key": "favorite_city",
"answer": "1"
}
]
}
]
}

@ -1,38 +0,0 @@
import './assets/main.css';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import ElementPlus from 'element-plus';
import App from './App.vue';
import router from './router';
// 创建 Pinia 实例并使用持久化插件
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
// 创建 Vue 应用
const app = createApp(App);
// 使用 Pinia 和路由
app.use(pinia);
app.use(router);
app.use(ElementPlus);
// 挂载应用
app.mount('#app');
// 在 main.js 或单独的文件中
import axios from 'axios';
// 添加请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
}, error => {
return Promise.reject(error);
});

@ -1,11 +0,0 @@
interface IUserInfo {
username: string;
password: string;
phone?: string;
isAdmin: boolean;
nickname?: string;
signature?: string;
birthday?: string;
}
export type { IUserInfo}

@ -1,124 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router';
// 动态导入组件
const HomeView = () => import('../views/HomeView.vue');
const Post = () => import('@/views/posts/post.vue');
const Login = () => import('../views/RegisterLogin/Login.vue');
const Register = () => import('../views/RegisterLogin/Register.vue');
const ForgotPassword = () => import('../views/RegisterLogin/ForgotPassword.vue');
const ResetPassword = () => import('../views/RegisterLogin/ResetPassword.vue');
const AdminLogin = () => import('../views/RegisterLogin/AdminLogin.vue');
const AdminForgotPassword = () => import('../views/RegisterLogin/AdminForgotPassword.vue');
const AdminResetPassword = () => import('../views/RegisterLogin/AdminResetPassword.vue');
const LogoutDialog = () => import('../views/RegisterLogin/LogoutDialog.vue');
const AdminView = () => import('../views/AdminView.vue');
const ProfileView = () => import('../views/ProfileView.vue');
const ArticleView = () => import('../views/ArticleView.vue');
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: HomeView,
},
{
path: '/post/:id', // 使用动态参数 :id
name: 'PostDetail',
component: Post,
props: true, // 将路由参数传递给组件
},
{
path: '/login',
name: 'Login',
component: Login,
},
{
path: '/register',
name: 'Register',
component: Register,
},
{
path: '/forgot-password',
name: 'ForgotPassword',
component: ForgotPassword,
},
{
path: '/reset-password',
name: 'ResetPassword',
component: ResetPassword,
},
{
path: '/Adminlogin',
name: 'AdminLogin',
component: AdminLogin,
},
{
path: '/Adminforgot-password',
name: 'AdminForgotPassword',
component: AdminForgotPassword,
},
{
path: '/Adminreset-password',
name: 'AdminResetPassword',
component: AdminResetPassword,
},
{
path: '/logout',
name: 'Logout',
component: LogoutDialog,
},
{
path: '/admin',
name: 'Admin',
component: AdminView,
children: [
{
path: '/profile',
name: 'AdminProfile',
component: ProfileView, // 保留作为管理员的子路由
},
{
path: '/article',
name: 'Article',
component: ArticleView,
},
]
},{
path: '/user',
name: 'User',
component: () => import('../views/UserView.vue'),
children: [
{
path: '/profile1',
name: 'Profile1',
component: () => import('../views/ProfileView1.vue'),
},
{
path: '/article1',
name: 'Article1',
component: () => import('../views/ArticleView.vue'),
},
{
path: '/edit',
name: 'Edit',
component: () => import('../views/edit.vue'),
},
{
path: '/display',
name: 'Display',
component: () => import('../views/display.vue'),
},
{
path: '/accounts',
name: 'accounts',
component: () => import('../views/accounts.vue'),
},
]
}
],
});
export default router;

@ -1,2 +0,0 @@
# .env
JWT_SECRET=your_secret_key_here

@ -1,55 +0,0 @@
<template>
<div class="common-layout">
<el-container>
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
<style>
.common-layout {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.el-header, .el-footer {
background-color: #c6e2ff;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #d9ecff;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
background-color: #ecf5ff;
color: #333;
text-align: center;
line-height: 160px;
}
body > .el-container {
margin-bottom: 40px;
}
.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}
.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
</style>

@ -1,878 +0,0 @@
{
"name": "backend-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "backend-server",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.3",
"cookie-session": "^2.1.0",
"cors": "^2.8.5",
"express": "^4.21.1"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-session": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/cookie-session/-/cookie-session-2.1.0.tgz",
"integrity": "sha512-u73BDmR8QLGcs+Lprs0cfbcAPKl2HnPcjpwRXT41sEV4DRJ2+W0vJEEZkG31ofkx+HZflA70siRIjiTdIodmOQ==",
"license": "MIT",
"dependencies": {
"cookies": "0.9.1",
"debug": "3.2.7",
"on-headers": "~1.0.2",
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cookie-session/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/cookie-session/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookies": {
"version": "0.9.1",
"resolved": "https://registry.npmmirror.com/cookies/-/cookies-0.9.1.tgz",
"integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"keygrip": "~1.1.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmmirror.com/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/keygrip": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/keygrip/-/keygrip-1.1.0.tgz",
"integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
"license": "MIT",
"dependencies": {
"tsscmp": "1.0.6"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"license": "MIT",
"engines": {
"node": ">=0.6.x"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

@ -1,27 +0,0 @@
{
"name": "lesson9",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.1",
"element-plus": "^1.0.2-beta.36",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0"
}
}

@ -1,110 +0,0 @@
// const jsonServer = require('json-server');
// const bodyParser = require('body-parser');
// const bcrypt = require('bcryptjs');
// const jwt = require('jsonwebtoken');
// const dotenv = require('dotenv');
// const app = jsonServer.create();
// const router = jsonServer.router('db.json');
// const middlewares = jsonServer.defaults();
// // 加载环境变量
// dotenv.config();
// // 使用中间件
// app.use(middlewares);
// app.use(bodyParser.urlencoded({ extended: true })); // 支持复杂的表单数据
// app.use(bodyParser.json());
// // 添加 CORS 头
// app.use((req, res, next) => {
// res.header('Access-Control-Allow-Origin', '*');
// res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// next();
// });
// // 获取用户数据
// let users = router.db.get('users').value();
// // 登录接口 (POST)
// app.post('/auth/login', async (req, res) => {
// const { account, password } = req.body;
// // 查找用户
// const user = users.find(u => u.account === account);
// if (!user) {
// return res.status(401).json({
// code: 0,
// message: '账号不存在'
// });
// }
// // 验证密码
// const isPasswordValid = await bcrypt.compare(password, user.password);
// if (!isPasswordValid) {
// return res.status(401).json({
// code: 0,
// message: '密码错误'
// });
// }
// // 生成 JWT 令牌
// const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
// // 返回成功响应
// res.status(200).json({
// code: 1,
// message: '登录成功',
// data: {
// id: user.id,
// account: user.account,
// token
// }
// });
// });
// // 密码重置接口 (PUT)
// app.put('/users/reset-password', async (req, res) => {
// console.log('Received reset-password request:', req.body); // 添加日志输出
// const { account, oldPassword, newPassword } = req.body;
// // 查找用户
// const userIndex = users.findIndex(u => u.account === account);
// if (userIndex === -1) {
// return res.status(404).json({
// code: 0,
// message: '用户未找到'
// });
// }
// const user = users[userIndex];
// // 验证旧密码
// const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password);
// if (!isOldPasswordValid) {
// return res.status(400).json({
// code: 0,
// message: '旧密码错误'
// });
// }
// // 对新密码进行哈希处理
// const hashedPassword = await bcrypt.hash(newPassword, 10);
// // 更新密码
// users[userIndex].password = hashedPassword;
// router.db.get('users').find({ id: user.id }).assign({ password: hashedPassword }).write();
// res.status(200).json({
// code: 1,
// message: '密码重置成功'
// });
// });
// // 启动服务器
// const port = 3000;
// app.listen(port, () => {
// console.log(`Server is running on http://localhost:${port}`);
// });

@ -1,32 +0,0 @@
// src/stores/authStore.js
import { defineStore } from 'pinia';
export const useAuthStore = defineStore('auth', {
state: () => ({
isAuthenticated: false, // 用户是否已登录
user: null, // 用户信息
}),
actions: {
async login(phone, password) {
try {
const response = await loginUser(phone, password);
this.isAuthenticated = true;
this.user = response.data; // 假设 API 返回的数据包含用户信息
ElMessage({
message: '登录成功',
type: 'success'
});
return true;
} catch (error) {
this.isAuthenticated = false;
this.user = null;
ElMessage.error('登录失败,请检查您的账号和密码');
throw error;
}
},
logout() {
this.isAuthenticated = false;
this.user = null;
}
}
});

@ -1,23 +0,0 @@
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true', // 从 localStorage 中恢复登录状态
phoneNumber: localStorage.getItem('phoneNumber') || '' // 从 localStorage 中恢复电话号码
}),
actions: {
setLoginState(value) {
this.isLoggedIn = !!value; // 确保 value 是布尔类型
localStorage.setItem('isLoggedIn', JSON.stringify(this.isLoggedIn)); // 将登录状态保存到 localStorage
console.log('Setting isLoggedIn to:', this.isLoggedIn); // 添加调试信息
},
setPhoneNumber(phoneNumber) {
this.phoneNumber = phoneNumber;
localStorage.setItem('phoneNumber', phoneNumber); // 将电话号码保存到 localStorage
console.log('Setting phoneNumber to:', this.phoneNumber); // 添加调试信息
},
getPhoneNumber() {
return this.phoneNumber || localStorage.getItem('phoneNumber') || ''; // 优先从 store 中获取,如果没有则从 localStorage 中获取
}
}
});

@ -1,17 +0,0 @@
// stores/index.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
isLoggedIn: false,
username: ''
}),
actions: {
setLoginState(isLoggedIn: boolean) {
this.isLoggedIn = isLoggedIn;
},
setUsername(username: string) {
this.username = username;
}
}
});

@ -1,25 +0,0 @@
// user.js (store)
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
isAuthenticated: false,
currentUser: null,
}),
actions: {
loginUser(username, password) {
// 假设这里的逻辑是调用后端API进行登录验证
// 登录成功后更新状态
this.isAuthenticated = true;
this.currentUser = { username, password }; // 简化示例,实际应包含更多用户信息
// 这里可以添加更多的业务逻辑,比如保存到本地存储等
},
logoutUser() {
this.isAuthenticated = false;
this.currentUser = null;
// 清除本地存储等操作
}
}
});

@ -1,56 +0,0 @@
// src/stores/user.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
interface IUser {
id: string;
adminname: string;
password: string;
phone: string;
signature: string;
birthday: string;
isAdmin: boolean;
security_questions: Array<{ question: string; answer: string }>;
}
export const useUserStore = defineStore('user', () => {
const currentUser = ref<IUser | null>(null);
const isLoggedIn = ref(false);
const isLoggedInAsAdmin = ref(false);
function setLoginState(isAdmin: boolean) {
isLoggedIn.value = true;
isLoggedInAsAdmin.value = isAdmin;
}
function logout() {
currentUser.value = null;
isLoggedIn.value = false;
isLoggedInAsAdmin.value = false;
}
function setCurrentUser(user: IUser) {
currentUser.value = user;
}
function getCurrentUser(): IUser | null {
return currentUser.value;
}
function updateUser(updatedUser: IUser) {
if (currentUser.value) {
Object.assign(currentUser.value, updatedUser);
}
}
return {
currentUser,
isLoggedIn,
isLoggedInAsAdmin,
setLoginState,
logout,
setCurrentUser,
getCurrentUser,
updateUser
};
});

@ -1,127 +0,0 @@
<template>
<el-container style="height: 100%;">
<!-- Header -->
<el-header class="header">
<el-row type="flex" justify="space-between" style="width:100%;" align="middle">
<!-- Logo -->
<el-col :span="6" class="logo-container">
<img src="@/assets/logo.png" alt="Logo" style="height: 120px;">
</el-col>
<!-- Logout Button -->
<el-col :span="6" style="text-align: right;">
<el-button type="danger" @click="logout">退</el-button>
</el-col>
</el-row>
</el-header>
<!-- Main Content -->
<el-container style="height: 80%;">
<!-- Aside (Left Sidebar) -->
<el-aside class="custom-aside">
<el-menu :default-active="activeMenu" :router="true" class="custom-menu">
<el-menu-item index="/">
<i class="el-icon-house"></i>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/profile">
<i class="el-icon-user"></i>
<span>个人信息</span>
</el-menu-item>
<el-menu-item index="/article">
<i class="el-icon-document"></i>
<span>文章管理</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- Main Section (Right Content) -->
<el-main>
<router-view />
</el-main>
</el-container>
<el-footer>
<span
style="height: 20px; position: fixed; bottom: 0; left: 0; right: 0; color: #333; text-align: center;">&copy;
2024 My Blog. All rights reserved.</span>
</el-footer>
</el-container>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user.ts';
const activeMenu = ref('/profile');
const router = useRouter();
const userStore = useUserStore();
onMounted(() => {
router.push(activeMenu.value)
})
const logout = () => {
router.push("/")
};
</script>
<style scoped>
.header {
height: 100px;
background-color: #ffffff;
border-bottom: 1px solid #ddd;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
/* 确保元素分布 */
overflow: hidden;
}
.logo {
height: 120px;
object-fit: contain;
overflow: hidden;
/* 防止 logo 超出容器 */
}
.el-header {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
}
.custom-aside {
width: 220px;
height:100vh;
background-color: white; /* 更亮的背景色 */
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); /* 添加阴影 */
}
.custom-menu {
border: none;
}
.custom-menu .el-menu-item {
margin: 10px 0;
border-radius: 4px;
transition: background-color 0.3s, color 0.3s; /* 平滑的动画效果 */
}
.custom-menu .el-menu-item:hover {
background-color: #e6f7ff; /* 悬停背景 */
color: #1890ff; /* 悬停字体颜色 */
}
.custom-menu .el-menu-item.is-active {
background-color: #bae7ff; /* 激活状态背景 */
color: #096dd9; /* 激活状态字体颜色 */
}
</style>

@ -1,296 +0,0 @@
<template>
<el-container>
<!-- Main Section (Right Content) -->
<el-main>
<el-table :data="articles" style="width: 100%">
<!-- 文章标题 -->
<el-table-column prop="title" label="文章标题" width="180"></el-table-column>
<!-- 文章作者 -->
<el-table-column prop="author" label="作者" width="120"></el-table-column>
<!-- 审核状态 -->
<el-table-column label="审核状态" width="120">
<template v-slot="scope">
<el-tag :type="getAuditStatusType(scope.row.status)" effect="dark">
<i :class="getAuditStatusIcon(scope.row.status)"></i>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<!-- 推荐状态 -->
<el-table-column label="推荐状态" width="120">
<template v-slot="scope">
<el-tag :type="getLikeStatusType(scope.row.like)" effect="dark">
<i :class="getLikeStatusIcon(scope.row.like)"></i>
{{ scope.row.like ? '已推荐' : '未推荐' }}
</el-tag>
</template>
</el-table-column>
<!-- 操作按钮 -->
<el-table-column label="操作">
<template v-slot="scope">
<el-button @click="showAuditView(scope.row)" type="primary">审核</el-button>
<el-button @click="confirmDeleteArticle(scope.row)" type="danger">删除</el-button>
<el-button @click="handleRecommendClick(scope.row)" class="recommend-button" type="info">推荐</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
<!-- 审核视图 -->
<div v-if="auditViewVisible" class="audit-view-overlay">
<div class="audit-view-content">
<h3>文章标题: {{ auditForm.title }}</h3>
<p>作者: {{ auditForm.author }}</p>
<p>提交时间: {{ auditForm.publishTime || '未提供' }}</p>
<p>文章内容:</p>
<p>{{ auditForm.content }}</p>
<el-form :model="auditForm" label-width="140px">
<el-form-item label="请输入审批未通过的理由:">
<el-input v-model="auditForm.reason" autocomplete="off" type="textarea"></el-input>
</el-form-item>
</el-form>
<div class="audit-actions">
<el-button @click="handleAuditSubmit('审核未通过')"></el-button>
<el-button type="primary" @click="handleAuditSubmit('审核通过')"></el-button>
<el-button @click="closeAuditView"></el-button>
</div>
</div>
</div>
</el-container>
<!-- 推荐对话框 -->
<el-dialog title="推荐文章" v-model:visible="recommendDialogVisible" width="30%" @close="resetRecommendForm">
<el-form :model="recommendForm">
<el-form-item label="请输入推荐理由:" :label-width="formLabelWidth">
<el-input v-model="recommendForm.reason" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="recommendDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleRecommendSubmit"></el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
//
const articles = ref([
{ id: 1, title: 'Vue 3入门', author: 'John Doe', status: '待审核', like: false, publishTime: '2024-11-01', content: '本文将介绍如何使用 Vue 3 构建高效的前端应用...' },
{ id: 2, title: 'JavaScript基础', author: 'Jane Doe', status: '审核通过', like: true, publishTime: '2024-11-02', content: '本文将介绍JavaScript的基础知识...' },
{ id: 3, title: '前端工程化', author: 'Alice Smith', status: '待审核', like: false, publishTime: '2024-11-03', content: '本文将介绍前端工程化的概念和技术...' },
]);
//
const getAuditStatusType = (status: string) => {
if (status === '审核通过') return 'success';
if (status === '审核未通过') return 'error';
return 'default';
};
//
const getAuditStatusIcon = (status: string) => {
if (status === '审核通过') return 'el-icon-check';
if (status === '审核未通过') return 'el-icon-close';
return '';
};
//
const getLikeStatusType = (like: boolean) => {
return like ? 'warning' : 'deault';
};
//
const getLikeStatusIcon = (like: boolean) => {
return like ? 'el-icon-star-on' : '';
};
//
const auditViewVisible = ref(false);
const auditForm = ref({ id: null, title: '', author: '', publishTime: '', content: '', reason: '' });
const showAuditView = (article: any) => {
console.log('审核按钮点击', article); //
auditViewVisible.value = true;
//
auditForm.value = { ...article, reason: '' };
};
const handleAuditSubmit = (status: string) => {
const article = articles.value.find((a) => a.id === auditForm.value.id);
if (article) {
article.status = status;
if (status === '审核未通过') {
article.reason = auditForm.value.reason;
ElMessage({
type: 'error',
message: `${article.title} ${status}`
});//
}
else{ ElMessage({
type: 'success',
message: `${article.title}${status}`
});
}
}
closeAuditView();
};
const closeAuditView = () => {
auditViewVisible.value = false;
resetAuditForm();
};
const resetAuditForm = () => {
auditForm.value = { id: null, title: '', author: '', publishTime: '', content: '', reason: '' };
};
//
const confirmDeleteArticle = (article: any) => {
ElMessageBox.confirm(
`确定要删除 "${article.title}" 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
//
const index = articles.value.indexOf(article);
if (index !== -1) {
articles.value.splice(index, 1); //
ElMessage({
type: 'success',
message: '删除成功!'
});
}
}).catch(() => {
ElMessage({
type: 'info',
message: '已取消删除'
});
});
};
//
const handleRecommendClick = (article: any) => {
const action = article.like ? '取消推荐' : '推荐';
const message = article.like ? `确定要取消推荐 "${article.title}" 吗?` : `确定要推荐 "${article.title}" 吗?`;
const messageType = article.like ? 'warning' : 'info';
ElMessageBox.confirm(
message,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: messageType
}
).then(() => {
//
article.like = !article.like;
if (article.like) {
//
showRecommendDialog(article);
ElMessage({
type: 'success',
message: '已推荐'
});
} else {
//
ElMessage({
type: 'info',
message: '已取消推荐'
});
}
}).catch(() => {
ElMessage({
type: 'info',
message: `已保留${action}状态`
});
});
};
//
const recommendDialogVisible = ref(false);
const recommendForm = ref({ id: null, reason: '' });
const formLabelWidth = ref('120px');
const showRecommendDialog = (article: any) => {
recommendDialogVisible.value = true;
recommendForm.value = { id: article.id, reason: '' }; // id
};
const handleRecommendSubmit = () => {
const article = articles.value.find((a) => a.id === recommendForm.value.id);
if (article && article.like) { //
article.reason = recommendForm.value.reason; //
ElMessage({
type: 'success',
message: `${article.title} 已成功推荐`
});
}
recommendDialogVisible.value = false;
};
const resetRecommendForm = () => {
recommendForm.value.reason = '';
};
</script>
<style scoped>
.el-header {
padding: 0;
}
.el-button {
margin-left: 10px;
}
/* 审核视图样式 */
.audit-view-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.audit-view-content {
background-color: white;
padding: 20px;
border-radius: 8px;
width: 60%; /* 增加宽度 */
max-width: 800px; /* 增加最大宽度 */
height: auto; /* 自适应高度 */
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.audit-actions {
margin-top: 20px;
text-align: right;
}
/* 新增:定义推荐按钮的样式 */
.el-button.recommend-button {
background-color: #e6a23c; /* 黄色背景 */
border-color: #e6a23c; /* 黄色边框 */
}
.el-button.recommend-button:hover {
background-color: darken(#e6a23c, 10%); /* 悬停时加深颜色 */
border-color: darken(#e6a23c, 10%);
}
</style>

@ -1,461 +0,0 @@
<template>
<el-container class="home">
<!-- 头部 -->
<el-header class="header">
<el-row type="flex" justify="space-between" align="middle" style="width:100%;">
<el-col :span="6" class="logo-container">
<img src="@/assets/logo.png" alt="Logo" class="logo">
</el-col>
<el-col :span="12" class="search-col">
<el-input placeholder="搜索..." prefix-icon="el-icon-search" size="large" class="search-input"></el-input>
</el-col>
<el-col :span="6" class="auth-buttons">
<div class="auth-content">
<div v-if="!isLogin">
<el-button link @click="handleAdminLogin" class="login-button">管理员</el-button>
<el-button link @click="handleLogin" class="login-button">登录</el-button>
<el-button type="primary" @click="handleRegister" class="register-button">注册</el-button>
</div>
<div v-else>
<el-link type="primary" :underline="false" @click="gotoHome" class="username-link">
{{ currentUser.username }}
</el-link>
<el-button type="danger" @click="handleLogout" class="logout-button">退出登录</el-button>
</div>
</div>
</el-col>
</el-row>
</el-header>
<!-- 轮播图 -->
<el-container>
<el-main>
<el-carousel trigger="click" height="400px" arrow="always" class="carousel">
<el-carousel-item v-for="(item, index) in images" :key="index">
<img :src="item" class="carousel-img" />
</el-carousel-item>
</el-carousel>
</el-main>
</el-container>
<!-- 文章内容部分 -->
<el-container class="content-container">
<el-aside width="400px" class="sidebar">
<h3 class="section-title">最新文章</h3>
<el-menu class="menu">
<el-menu-item v-for="post in latestPosts" :key="post.id">
<router-link :to="'/post/' + post.id" class="link-item">{{ post.title }}</router-link>
</el-menu-item>
</el-menu>
<h3 class="section-title">热门文章</h3>
<el-menu class="menu">
<el-menu-item v-for="post in popularPosts" :key="post.id">
<router-link :to="'/post/' + post.id" class="link-item">{{ post.title }}</router-link>
</el-menu-item>
</el-menu>
<h3 class="section-title">推荐文章</h3>
<el-menu class="menu">
<el-menu-item v-for="post in recommendedPosts" :key="post.id">
<router-link :to="'/post/' + post.id" class="link-item">{{ post.title }}</router-link>
</el-menu-item>
</el-menu>
<el-divider></el-divider>
</el-aside>
<el-main class="posts-container">
<div v-for="post in posts" :key="post.id" class="blog-post">
<router-link :to="'/post/' + post.id">
<h3>{{ post.title }}</h3>
</router-link>
<p><strong>Author:</strong> {{ post.author }} | <strong>Published:</strong> {{ post.date }}</p>
<p>{{ post.excerpt }}</p>
</div>
</el-main>
</el-container>
<!-- 底部 -->
<el-footer class="footer">
<span>&copy; 2024 My Blog. All rights reserved.</span>
</el-footer>
</el-container>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed } from 'vue'; // computed
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user';
import { Article } from '../../api/articleApi';
//
import img1 from '@/assets/carousel/1.jpg';
import img2 from '@/assets/carousel/2.jpg';
import img3 from '@/assets/carousel/3.jpg';
import img4 from '@/assets/carousel/4.jpg';
import img5 from '@/assets/carousel/5.jpg';
import img6 from '@/assets/carousel/6.jpg';
const images = ref([img1, img2, img3, img4, img5, img6]);
const latestPosts = ref<Article[]>([]);
const popularPosts = ref<Article[]>([]);
const recommendedPosts = ref<Article[]>([]);
const posts = ref<Article[]>([]);
const store = useUserStore();
const router = useRouter();
// 使 computed
const isLogin = computed(() => store.isAuthenticated);
const currentUser = computed(() => store.currentUser);
const fetchArticles = async () => {
try {
const [latestRes, popularRes, recommendedRes, allArticlesRes] = await Promise.all([
articleApi.getLatestArticles(),
articleApi.getPopularArticles(),
articleApi.getRecommendedArticles(),
articleApi.getAllArticles()
]);
latestPosts.value = latestRes.data;
popularPosts.value = popularRes.data;
recommendedPosts.value = recommendedRes.data;
posts.value = allArticlesRes.data;
} catch (error) {
console.error('Failed to fetch articles:', error);
}
};
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString();
};
//
const handleLogin = () => {
router.push({ name: "Login" });
};
const handleAdminLogin = () => {
router.push("/Adminlogin");
};
const handleRegister = () => {
router.push("/register");
};
const handleLogout = () => {
store.logout();
router.push('/');
};
const gotoHome = () => {
router.push('/');
};
onMounted(() => {
fetchArticles();
});
</script>
<style scoped>
/* 页面基础样式 */
.home {
height: 100%;
width: 100%;
font-family: 'Arial', sans-serif;
background-image: url('@/assets/bg1.jpg'); /* Path to your background image */
background-size: cover; /* Ensures the image covers the entire page */
background-position: center; /* Centers the image */
background-repeat: no-repeat;
/* 设置背景色 */
overflow-x: hidden;
/* 防止内容横向溢出 */
}
/* Header 样式 */
.header {
height: 100px;
background-color: rgba(255, 255, 255, 0.8);;
border-bottom: 1px solid #ddd;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
/* 确保元素分布 */
overflow: hidden;
}
.logo {
height: 120px;
object-fit: contain;
overflow: hidden;
/* 防止 logo 超出容器 */
}
.search-input {
width: 100%;
border-radius: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
font-size: 16px;
}
/* 登录注册按钮 */
.auth-buttons {
display: flex;
justify-content: flex-end;
align-items: center;
}
.login-button,
.register-button,
.logout-button {
margin-left: 15px;
}
.username-link {
margin-right: 15px;
}
/* 轮播图样式 */
.carousel {
margin-bottom: 20px;
overflow: hidden;
/* 遮住轮播图超出的部分 */
}
.carousel-img {
width: 100%;
height: 400px;
object-fit: cover;
border-radius: 8px;
max-height: 100%;
/* 确保图片不会溢出容器 */
}
/* 内容容器 */
.content-container {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding: 0 20px;
}
.sidebar {
background-color: rgba(255, 255, 255, 0.8);;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 280px;
/* 控制侧边栏的宽度 */
overflow: hidden;
/* 遮住超出部分 */
}
.menu {
background-color: rgba(255, 255, 255, 0);;
margin-bottom: 15px;
}
.section-title {
font-size: 20px;
color: #333;
margin-bottom: 10px;
border-bottom: 2px solid #ddd;
padding-bottom: 10px;
font-weight: 600;
word-wrap: break-word;
/* 防止标题溢出 */
white-space: normal;
/* 保证标题文本正常换行 */
overflow: hidden;
/* 遮住超出部分 */
}
.link-item {
text-decoration: none;
color: #333;
font-size: 16px;
transition: color 0.3s ease, padding-left 0.3s ease;
display: block;
/* 使链接成为块级元素 */
padding: 8px 0;
/* 给链接增加上下内边距 */
overflow: hidden;
/* 遮住超出部分 */
}
.link-item:hover {
color: #1a73e8;
padding-left: 10px;
}
/* 帖子列表容器 */
.posts-container {
flex: 1;
padding: 20px;
background-color:rgba(255, 255, 255, 0.8);;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
/* 遮住超出部分 */
}
/* 博客文章样式 */
.blog-post {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #ddd;
padding-left: 20px;
padding-right: 20px;
overflow: hidden;
/* 遮住超出部分 */
transition: background-color 0.3s ease, transform 0.2s ease;
}
.blog-post:hover {
background-color: #f9f9f9;
/* 鼠标悬浮时背景色变化 */
transform: translateY(-5px);
/* 鼠标悬浮时向上浮动 */
}
/* 文章标题样式 */
.blog-post h3 {
font-size: 22px;
color: #1a73e8;
margin-top: 10px;
margin-bottom: 10px;
transition: color 0.3s ease;
word-wrap: break-word;
/* 防止标题溢出 */
white-space: normal;
/* 保证标题文本正常换行 */
overflow: hidden;
/* 遮住超出部分 */
}
.blog-post h3:hover {
color: #0066cc;
/* 悬浮时标题颜色变化 */
cursor: pointer;
}
/* 文章元数据样式 (作者和发布时间) */
.blog-post p {
color: #555;
font-size: 14px;
line-height: 1.6;
margin: 5px 0;
}
.blog-post p strong {
font-weight: 600;
color: #333;
}
/* 文章摘要样式 */
.blog-post .excerpt {
color: #666;
font-size: 16px;
line-height: 1.8;
margin-top: 10px;
display: -webkit-box;
-webkit-line-clamp: 3;
/* 限制显示的行数 */
-webkit-box-orient: vertical;
overflow: hidden;
/* 超出的部分隐藏 */
text-overflow: ellipsis;
/* 省略号 */
}
.blog-post .excerpt:hover {
text-decoration: underline;
/* 悬浮时摘要增加下划线 */
}
/* 响应式布局:适应小屏幕 */
@media (max-width: 768px) {
.posts-container {
padding: 10px;
}
.blog-post {
padding-left: 10px;
padding-right: 10px;
}
.blog-post h3 {
font-size: 20px;
}
.blog-post p {
font-size: 13px;
}
}
/* Footer 样式 */
.footer {
background-color: #ffffff;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
color: #333;
font-size: 14px;
border-top: 1px solid #ddd;
}
.footer span {
padding: 10px;
}
/* 响应式布局:适应小屏幕 */
@media (max-width: 768px) {
.header {
height: 80px;
}
.logo-container {
display: block;
text-align: center;
}
.content-container {
flex-direction: column;
}
.sidebar {
width: 100%;
margin-bottom: 20px;
}
.posts-container {
width: 100%;
}
.carousel-img {
height: 300px;
}
.menu {
padding: 0 10px;
}
}
a {
text-decoration: none;
}
</style>

@ -1,180 +0,0 @@
<template>
<div class="personal-info">
<div class="info-container">
<!-- 头像 -->
<div class="avatar-container">
<img class="avatar" src="../assets/user/avatar.png" alt="User Avatar" />
</div>
<!-- 用户信息 -->
<div class="user-info">
<p class="user-item"><strong>账户类型:管理员</strong> </p>
<!-- 可编辑的昵称 -->
<p class="user-item">
<strong>昵称:</strong>
<input v-model="currentUser.username" class="editable-input" type="text" />
</p>
<!-- 可编辑的个性签名 -->
<p class="user-item">
<strong>个性签名:</strong>
<input v-model="currentUser.signature" class="editable-input" type="text" />
</p>
<!-- 手机号码 (管理员和普通用户都显示) -->
<p class="user-item">
<strong>手机号码:</strong>
<input v-model="currentUser.phone" class="editable-input" type="text" disabled/>
</p>
<!-- 可编辑的生日 -->
<p class="user-item">
<strong>生日:</strong>
<input v-model="currentUser.birthday" class="editable-input" type="date" />
</p>
<!-- 安全问题 (管理员和普通用户都显示) -->
<div class="security-questions">
<p v-for="(question, index) in currentUser.security_questions" :key="index" class="user-item">
<strong>{{ question.question }}:</strong>
<input v-model="question.answer" class="editable-input" type="text" />
</p>
</div>
<p class="user-item">
<el-button type="primary" @click="saveUser" style="width: 100%;">保存</el-button>
</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive } from 'vue';
import { useUserStore } from '../stores/user';
import { IUserInfo } from '../model/model';
import axios from 'axios';
// Piniauser store
const userStore = useUserStore();
const currentUser = reactive<IUserInfo>({
id: '',
username: '',
password: '',
phone: '',
signature: '',
birthday: '',
isAdmin: false,
security_questions: []
});
onMounted(() => {
//
const user = userStore.getCurrentUser();
if (user && user.username) {
fetchUserInfo(user.username);
}
});
const fetchUserInfo = async (username: string) => {
try {
let apiUrl = '';
if (userStore.isLoggedInAsAdmin) {
apiUrl = `http://localhost:3003/admins?username=${username}`;
} else {
apiUrl = `http://localhost:3002/users?username=${username}`;
}
const response = await axios.get(apiUrl);
if (response.data.length > 0) {
const user = response.data[0];
Object.assign(currentUser, user);
currentUser.isAdmin = userStore.isLoggedInAsAdmin; //
console.log("Fetched user info:", currentUser);
} else {
console.error('No user found with the provided username.');
}
} catch (error) {
console.error('Error fetching user info:', error);
}
}
const saveUser = async () => {
try {
let apiUrl = '';
if (currentUser.isAdmin) {
apiUrl = `http://localhost:3003/admins/${currentUser.id}`;
} else {
apiUrl = `http://localhost:3002/users/${currentUser.id}`;
}
await axios.put(apiUrl, currentUser);
console.log('用户信息已更新:', currentUser);
ElMessage.success("保存成功");
} catch (error) {
console.error('Error updating user info:', error);
ElMessage.error("保存失败,请稍后再试");
}
}
</script>
<style scoped>
/* 整体布局居中 */
.personal-info {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.info-container {
background-color: #fff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: left;
width: 350px;
}
.avatar-container {
text-align: center;
margin-bottom: 20px;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
font-size: 16px;
color: #333;
}
.user-item {
font-size: 16px;
margin-bottom: 10px;
}
.user-item strong {
color: #555;
}
/* 样式修改:让输入框看起来更整洁 */
.editable-input {
width: 100%;
padding: 8px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.security-questions {
margin-top: 10px;
}
</style>

@ -1,181 +0,0 @@
<template>
<div class="personal-info">
<div class="info-container">
<!-- 头像 -->
<div class="avatar-container">
<img v-if="currentUser.isAdmin" class="avatar" src="../assets/user/avatar.png" alt="User Avatar" />
<img v-else class="avatar" src="../assets/user/user.png" alt="User Avatar" />
</div>
<!-- 用户信息 -->
<div class="user-info">
<p class="user-item"><strong>账户类型:普通用户</strong></p>
<!-- 可编辑的昵称 -->
<p class="user-item">
<strong>昵称:</strong>
<input v-model="currentUser.username" class="editable-input" type="text" />
</p>
<!-- 可编辑的个性签名 -->
<p class="user-item">
<strong>个性签名:</strong>
<input v-model="currentUser.signature" class="editable-input" type="text" />
</p>
<!-- 手机号码 (管理员和普通用户都显示) -->
<p class="user-item">
<strong>手机号码:</strong>
<input v-model="currentUser.phone" class="editable-input" type="text" disabled/>
</p>
<!-- 可编辑的生日 -->
<p class="user-item">
<strong>生日:</strong>
<input v-model="currentUser.birthday" class="editable-input" type="date" />
</p>
<!-- 安全问题 (管理员和普通用户都显示) -->
<div class="security-questions">
<p v-for="(question, index) in currentUser.security_questions" :key="index" class="user-item">
<strong>{{ question.question }}:</strong>
<input v-model="question.answer" class="editable-input" type="text" />
</p>
</div>
<p class="user-item">
<el-button type="primary" @click="saveUser" style="width: 100%;">保存</el-button>
</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive } from 'vue';
import { useUserStore } from '../stores/user';
import { IUserInfo } from '../model/model';
import axios from 'axios';
// Piniauser store
const userStore = useUserStore();
const currentUser = reactive<IUserInfo>({
id: '',
username: '',
password: '',
phone: '',
signature: '',
birthday: '',
isAdmin: false,
security_questions: []
});
onMounted(() => {
//
const user = userStore.getCurrentUser();
if (user && user.username) {
fetchUserInfo(user.username);
}
});
const fetchUserInfo = async (username: string) => {
try {
let apiUrl = '';
if (userStore.isLoggedInAsAdmin) {
apiUrl = `http://localhost:3003/admins?username=${username}`;
} else {
apiUrl = `http://localhost:3002/users?username=${username}`;
}
const response = await axios.get(apiUrl);
if (response.data.length > 0) {
const user = response.data[0];
Object.assign(currentUser, user);
currentUser.isAdmin = userStore.isLoggedInAsAdmin; //
console.log("Fetched user info:", currentUser);
} else {
console.error('No user found with the provided username.');
}
} catch (error) {
console.error('Error fetching user info:', error);
}
}
const saveUser = async () => {
try {
let apiUrl = '';
if (currentUser.isAdmin) {
apiUrl = `http://localhost:3003/admins/${currentUser.id}`;
} else {
apiUrl = `http://localhost:3002/users/${currentUser.id}`;
}
await axios.put(apiUrl, currentUser);
console.log('用户信息已更新:', currentUser);
ElMessage.success("保存成功");
} catch (error) {
console.error('Error updating user info:', error);
ElMessage.error("保存失败,请稍后再试");
}
}
</script>
<style scoped>
/* 整体布局居中 */
.personal-info {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.info-container {
background-color: #fff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: left;
width: 350px;
}
.avatar-container {
text-align: center;
margin-bottom: 20px;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
.user-info {
font-size: 16px;
color: #333;
}
.user-item {
font-size: 16px;
margin-bottom: 10px;
}
.user-item strong {
color: #555;
}
/* 样式修改:让输入框看起来更整洁 */
.editable-input {
width: 100%;
padding: 8px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.security-questions {
margin-top: 10px;
}
</style>

@ -1,284 +0,0 @@
<template>
<el-main class="login-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 10px; margin-top: -10px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 10px;">
{{ step === 1 ? '忘记密码-页面1' : '忘记密码-页面2' }}
</div>
<el-form :model="form" status-icon :rules="currentRules" ref="formRef" label-width="96px" label-position="right">
<el-form-item v-if="step === 1" label="用户的账号:" prop="adminname">
<el-input v-model="form.adminname" placeholder="请输入账号名" @blur="fetchSecurityQuestions"></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="第一个问题:" prop="question1">
<el-input v-model="form.question1" disabled></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="第一个答案:" prop="answer1">
<el-input v-model="form.answer1" placeholder="请回答第一个问题"></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="第二个问题:" prop="question2">
<el-input v-model="form.question2" disabled></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="第二个答案:" prop="answer2">
<el-input v-model="form.answer2" placeholder="请回答第二个问题"></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="设置新密码:" prop="newPassword">
<el-input type="password" v-model="form.newPassword" placeholder="请输入新密码"></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="确认新密码:" prop="confirmPassword">
<el-input type="password" v-model="form.confirmPassword" placeholder="请再次输入新密码"></el-input>
</el-form-item>
<div v-if="step === 1" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<div v-if="step === 1" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-button style="position: relative; width: 35%;" @click="navigateToLogin">
取消
</el-button>
<el-button type="primary" style="position: relative; width: 35%; background-color: #dc99a1;" @click="nextStep">
下一步
</el-button>
</div>
<div v-if="step === 2" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<div v-if="step === 2" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-button style="position: relative; width: 35%;" @click="previousStep">
上一步
</el-button>
<el-button type="primary" style="position: relative; width: 35%; background-color: #dc99a1;" @click="submitForm">
确认
</el-button>
</div>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { reactive, ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const form = reactive({
adminname: '',
question1: '',
answer1: '',
question2: '',
answer2: '',
newPassword: '',
confirmPassword: ''
});
const formRef = ref(null);
const step = ref(1);
const loading = ref(false); //
const validateAnswer1 = (rule, value, callback) => {
if (!value) {
return callback(new Error('请回答第一个问题'));
}
if (adminSecurityQuestions.value && adminSecurityQuestions.value[0] && value !== adminSecurityQuestions.value[0].answer) {
return callback(new Error('第一个问题答案错误'));
}
callback();
};
const validateAnswer2 = (rule, value, callback) => {
if (!value) {
return callback(new Error('请回答第二个问题'));
}
if (adminSecurityQuestions.value && adminSecurityQuestions.value[1] && value !== adminSecurityQuestions.value[1].answer) {
return callback(new Error('第二个问题答案错误'));
}
callback();
};
const validateConfirmPassword = (rule, value, callback) => {
if (value !== form.newPassword) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};
const rules = reactive({
adminname: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
answer1: [
{ validator: validateAnswer1, trigger: 'blur' }
],
answer2: [
{ validator: validateAnswer2, trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
});
const currentRules = computed(() => {
if (step.value === 1) {
return {
adminname: rules.adminname,
answer1: rules.answer1
};
} else {
return {
answer2: rules.answer2,
newPassword: rules.newPassword,
confirmPassword: rules.confirmPassword
};
}
});
const adminSecurityQuestions = ref(null);
const fetchSecurityQuestions = async () => {
loading.value = true;
try {
const response = await axios.get(`http://localhost:3003/admins?adminname=${form.adminname}`);
const admins = response.data;
if (admins.length > 0) {
const admin = admins[0];
adminSecurityQuestions.value = admin.security_questions;
form.question1 = admin.security_questions[0]?.question || '';
form.question2 = admin.security_questions[1]?.question || '';
} else {
ElMessageBox.alert('用户不存在', '提示', {
confirmButtonText: '确定'
});
form.question1 = '';
form.question2 = '';
resetForm();
}
} catch (error) {
if (error.response && error.response.status === 404) {
ElMessageBox.alert('模拟后端未正确启动或请求 URL 错误', '提示', {
confirmButtonText: '确定'
});
} else {
ElMessageBox.alert(`获取安全问题失败: ${error.message}`, '提示', {
confirmButtonText: '确定'
});
}
} finally {
loading.value = false;
}
};
const nextStep = async () => {
const valid = await new Promise((resolve) => {
formRef.value.validate(resolve);
});
if (valid) {
step.value = 2;
} else {
ElMessage.error('请检查输入信息');
}
};
const previousStep = () => {
step.value = 1;
};
const submitForm = async () => {
const valid = await new Promise((resolve) => {
formRef.value.validate(resolve);
});
if (valid) {
try {
const response = await axios.get(`http://localhost:3003/admins?adminname=${form.adminname}`);
const admins = response.data;
if (admins.length > 0) {
const admin = admins[0];
const answersMatch = admin.security_questions.every((q, index) => {
return q.answer === (index === 0 ? form.answer1 : form.answer2);
});
if (answersMatch) {
//
await axios.put(`http://localhost:3003/admins/${admin.id}`, {
...admin, //
password: form.newPassword //
});
ElMessage.success('密码重置成功');
router.push('/login');
resetForm();
} else {
ElMessageBox.alert('安全问题答案错误', '提示', {
confirmButtonText: '确定'
});
}
} else {
ElMessageBox.alert('用户不存在', '提示', {
confirmButtonText: '确定'
});
}
} catch (error) {
if (error.response && error.response.status === 404) {
ElMessageBox.alert('模拟后端未正确启动或请求 URL 错误', '提示', {
confirmButtonText: '确定'
});
} else {
ElMessageBox.alert(`安全问题验证失败: ${error.message}`, '提示', {
confirmButtonText: '确定'
});
}
}
} else {
ElMessageBox.alert('请正确填写所有信息', '提示', {
confirmButtonText: '确定'
});
}
};
const navigateToLogin = () => {
router.push('/Adminlogin');
};
const navigateToResetPassword = () => {
router.push('/Adminreset-password');
};
const resetForm = () => {
formRef.value.resetFields();
};
</script>
<style scoped>
.login-container {
background-image: url('../public/背景6.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,128 +0,0 @@
<template>
<el-main class="login-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 20px; margin-top: -10px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 20px;">
管理员登录
</div>
<el-form :model="form" status-icon :rules="rules" ref="formRef" label-width="70px">
<el-form-item label="账号:" prop="phone" style="margin-bottom: 20px !important;">
<el-input v-model="form.phone" placeholder="请输入管理员账号(11位手机号码)"></el-input>
</el-form-item>
<el-form-item label="密码:" prop="password" style="margin-bottom: 20px !important;">
<el-input type="password" v-model="form.password" placeholder="请输入账号密码"></el-input>
</el-form-item>
<div style="display: flex; justify-content: space-between;margin-left: 20px; margin-top: 10px; margin-bottom: 20px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<el-form-item>
<el-button style="position: relative; width: 45%; margin-left: -50px;margin-right:72px;" @click="navigateToHome">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">取消</span>
</el-button>
<el-button type="primary" style="position: relative; width: 45%; background-color: #dc99a1;" @click="submitForm">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">登录</span>
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { loginUser } from '../../../api/userApi'; // userApi.js loginUser
import { useUserStore } from '../../stores/index.ts';
const router = useRouter();
const userStore = useUserStore();
const formRef = ref(null);
const errorMessage = ref('');
//
const form = reactive({
phone: '', //
password: ''
});
//
const rules = reactive({
phone: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的11位手机号码', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
]
});
//
const submitForm = async () => {
try {
await formRef.value.validate(); //
const response = await loginUser(form.phone, form.password);
//
ElMessage({
message: '登录成功',
type: 'success'
});
//
router.push('/admin');
} catch (error) {
errorMessage.value = error.message;
ElMessage.error('登录失败,请检查您的账号和密码');
}
};
//
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
};
//
const navigateToResetPassword = () => {
router.push('/reset-password');
};
//
const navigateToHome = () => {
router.push('/');
};
</script>
<style scoped>
.login-container {
background-image: url('../public/背景5.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(3px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,173 +0,0 @@
<template>
<el-main class="adminlogin-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 20px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 20px;">
管理员重置密码
</div>
<el-form :model="form" status-icon :rules="rules" ref="formRef" label-width="82px" label-position="left">
<el-form-item label="手机号码:" prop="phone">
<el-input v-model="form.phone" placeholder="请输入11位手机号码"></el-input>
</el-form-item>
<el-form-item label="旧的密码:" prop="oldPassword">
<el-input type="password" v-model="form.oldPassword" placeholder="请输入旧密码"></el-input>
</el-form-item>
<el-form-item label="新的密码:" prop="newPassword">
<el-input type="password" v-model="form.newPassword" placeholder="请输入新密码"></el-input>
</el-form-item>
<el-form-item label="确认密码:" prop="confirmPassword">
<el-input type="password" v-model="form.confirmPassword" placeholder="请再次输入新密码"></el-input>
</el-form-item>
<div style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 20px;">
<el-link type="primary" @click="navigateToForgotPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<el-form-item>
<el-button style="width: 45%; margin-left: -84px; margin-right: 54px;" @click="navigateToLogin"></el-button>
<el-button type="primary" style="width: 45%; margin-left: 52px; background-color: #dc99a1;" @click="submitForm"></el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const form = reactive({
phone: '', // phone
oldPassword: '', //
newPassword: '',
confirmPassword: ''
});
const formRef = ref(null);
//
const validateConfirmPassword = (rule, value, callback) => {
if (value !== form.newPassword) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};
const rules = reactive({
phone: [ // phone
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
oldPassword: [ //
{ required: true, message: '请输入旧密码', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
});
const submitForm = async () => {
const valid = await new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
resolve(true);
} else {
resolve(false);
}
});
});
if (valid) {
try {
//
const admins = await axios.get('http://localhost:3003/admins');
const admin = admins.data.find(admin => admin.phone === form.phone && admin.password === form.oldPassword); //
if (!admin) {
ElMessageBox.alert('用户名或旧密码错误', '提示', {
confirmButtonText: '确定'
});
return;
}
//
const updatedadmin = {
...admin, //
password: form.newPassword //
};
// PUT
await axios.put(`http://localhost:3003/admins/${admin.id}`, updatedadmin);
//
ElMessageBox.alert('密码重置成功', '提示', {
confirmButtonText: '确定',
callback: () => {
router.push('/login');
resetForm();
}
});
} catch (error) {
if (error.response && error.response.status === 404) {
ElMessageBox.alert('模拟后端未正确启动或请求 URL 错误', '提示', {
confirmButtonText: '确定'
});
} else {
ElMessageBox.alert(`密码重置失败: ${error.message}`, '提示', {
confirmButtonText: '确定'
});
}
}
} else {
ElMessageBox.alert('密码重置失败,请检查输入信息', '提示', {
confirmButtonText: '确定'
});
}
};
const navigateToLogin = () => {
router.push('/Adminlogin');
};
const navigateToForgotPassword = () => {
router.push('/Adminforgot-password');
};
const resetForm = () => {
formRef.value.resetFields();
};
</script>
<style scoped>
.adminlogin-container {
background-image: url('../public/背景8.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,284 +0,0 @@
<template>
<el-main class="login-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 10px; margin-top: -10px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 10px;">
{{ step === 1 ? '忘记密码-页面1' : '忘记密码-页面2' }}
</div>
<el-form :model="form" status-icon :rules="currentRules" ref="formRef" label-width="96px" label-position="right">
<el-form-item v-if="step === 1" label="用户的账号:" prop="phone">
<el-input v-model="form.phone" placeholder="请输入账号(11位手机号码)" @blur="fetchSecurityQuestions"></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="第一个问题:" prop="question1">
<el-input v-model="form.question1" disabled></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="第一个答案:" prop="answer1">
<el-input v-model="form.answer1" placeholder="请回答第一个问题"></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="第二个问题:" prop="question2">
<el-input v-model="form.question2" disabled></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="第二个答案:" prop="answer2">
<el-input v-model="form.answer2" placeholder="请回答第二个问题"></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="设置新密码:" prop="newPassword">
<el-input type="password" v-model="form.newPassword" placeholder="请输入新密码"></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="确认新密码:" prop="confirmPassword">
<el-input type="password" v-model="form.confirmPassword" placeholder="请再次输入新密码"></el-input>
</el-form-item>
<div v-if="step === 1" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<div v-if="step === 1" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-button style="position: relative; width: 35%;" @click="navigateToLogin">
取消
</el-button>
<el-button type="primary" style="position: relative; width: 35%; background-color: #dc99a1;" @click="nextStep">
下一步
</el-button>
</div>
<div v-if="step === 2" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<div v-if="step === 2" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-button style="position: relative; width: 35%;" @click="previousStep">
上一步
</el-button>
<el-button type="primary" style="position: relative; width: 35%; background-color: #dc99a1;" @click="submitForm">
确认
</el-button>
</div>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { reactive, ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const form = reactive({
phone: '',
question1: '',
answer1: '',
question2: '',
answer2: '',
newPassword: '',
confirmPassword: ''
});
const formRef = ref(null);
const step = ref(1);
const loading = ref(false); //
const validateAnswer1 = (rule, value, callback) => {
if (!value) {
return callback(new Error('请回答第一个问题'));
}
if (userSecurityQuestions.value && userSecurityQuestions.value[0] && value !== userSecurityQuestions.value[0].answer) {
return callback(new Error('第一个问题答案错误'));
}
callback();
};
const validateAnswer2 = (rule, value, callback) => {
if (!value) {
return callback(new Error('请回答第二个问题'));
}
if (userSecurityQuestions.value && userSecurityQuestions.value[1] && value !== userSecurityQuestions.value[1].answer) {
return callback(new Error('第二个问题答案错误'));
}
callback();
};
const validateConfirmPassword = (rule, value, callback) => {
if (value !== form.newPassword) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};
const rules = reactive({
phone: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
answer1: [
{ validator: validateAnswer1, trigger: 'blur' }
],
answer2: [
{ validator: validateAnswer2, trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
});
const currentRules = computed(() => {
if (step.value === 1) {
return {
phone: rules.phone,
answer1: rules.answer1
};
} else {
return {
answer2: rules.answer2,
newPassword: rules.newPassword,
confirmPassword: rules.confirmPassword
};
}
});
const userSecurityQuestions = ref(null);
const fetchSecurityQuestions = async () => {
loading.value = true;
try {
const response = await axios.get(`http://localhost:3002/users?phone=${form.phone}`);
const users = response.data;
if (users.length > 0) {
const user = users[0];
userSecurityQuestions.value = user.security_questions;
form.question1 = user.security_questions[0]?.question || '';
form.question2 = user.security_questions[1]?.question || '';
} else {
ElMessageBox.alert('用户不存在', '提示', {
confirmButtonText: '确定'
});
form.question1 = '';
form.question2 = '';
resetForm();
}
} catch (error) {
if (error.response && error.response.status === 404) {
ElMessageBox.alert('模拟后端未正确启动或请求 URL 错误', '提示', {
confirmButtonText: '确定'
});
} else {
ElMessageBox.alert(`获取安全问题失败: ${error.message}`, '提示', {
confirmButtonText: '确定'
});
}
} finally {
loading.value = false;
}
};
const nextStep = async () => {
const valid = await new Promise((resolve) => {
formRef.value.validate(resolve);
});
if (valid) {
step.value = 2;
} else {
ElMessage.error('请检查输入信息');
}
};
const previousStep = () => {
step.value = 1;
};
const submitForm = async () => {
const valid = await new Promise((resolve) => {
formRef.value.validate(resolve);
});
if (valid) {
try {
const response = await axios.get(`http://localhost:3002/users?phone=${form.phone}`);
const users = response.data;
if (users.length > 0) {
const user = users[0];
const answersMatch = user.security_questions.every((q, index) => {
return q.answer === (index === 0 ? form.answer1 : form.answer2);
});
if (answersMatch) {
//
await axios.put(`http://localhost:3002/users/${user.id}`, {
...user, //
password: form.newPassword //
});
ElMessage.success('密码重置成功');
router.push('/login');
resetForm();
} else {
ElMessageBox.alert('安全问题答案错误', '提示', {
confirmButtonText: '确定'
});
}
} else {
ElMessageBox.alert('用户不存在', '提示', {
confirmButtonText: '确定'
});
}
} catch (error) {
if (error.response && error.response.status === 404) {
ElMessageBox.alert('模拟后端未正确启动或请求 URL 错误', '提示', {
confirmButtonText: '确定'
});
} else {
ElMessageBox.alert(`安全问题验证失败: ${error.message}`, '提示', {
confirmButtonText: '确定'
});
}
}
} else {
ElMessageBox.alert('请正确填写所有信息', '提示', {
confirmButtonText: '确定'
});
}
};
const navigateToLogin = () => {
router.push('/login');
};
const navigateToResetPassword = () => {
router.push('/reset-password');
};
const resetForm = () => {
formRef.value.resetFields();
};
</script>
<style scoped>
.login-container {
background-image: url('../public/背景4.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,120 +0,0 @@
<template>
<el-main class="login-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 20px; margin-top: -10px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 20px;">
用户登录
</div>
<el-form :model="form" status-icon :rules="rules" ref="formRef" label-width="70px">
<el-form-item label="账号:" prop="phone" style="margin-bottom: 20px !important;">
<el-input v-model="form.phone" placeholder="请输入11位手机号码"></el-input>
</el-form-item>
<el-form-item label="密码:" prop="password" style="margin-bottom: 20px !important;">
<el-input type="password" v-model="form.password" placeholder="请输入账号密码"></el-input>
</el-form-item>
<div style="display: flex; justify-content: space-between;margin-left: 20px; margin-top: 10px; margin-bottom: 20px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<el-form-item>
<el-button style="position: relative; width: 45%; margin-left: -50px;margin-right:72px;" @click="navigateToHome">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">取消</span>
</el-button>
<el-button type="primary" style="position: relative; width: 45%; background-color: #dc99a1;" @click="submitForm">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">登录</span>
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { loginUser } from '../../../api/userApi';
const router = useRouter();
const formRef = ref(null);
const errorMessage = ref('');
const form = reactive({
phone: '',
password: ''
});
const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输入正确的11位手机号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
]
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
};
const navigateToResetPassword = () => {
router.push('/reset-password');
};
//
const navigateToHome = () => {
router.push('/');
};
const submitForm = async () => {
try {
await formRef.value.validate(); //
const response = await loginUser(form.phone, form.password);
ElMessage({
message: '登录成功',
type: 'success'
});
//
router.push('/profile1');
} catch (error) {
errorMessage.value = error.message;
ElMessage.error('登录失败,请检查您的账号和密码');
}
};
</script>
<style scoped>
.login-container {
background-image: url('../public/背景1.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(3px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,141 +0,0 @@
<template>
<el-main class="login-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 20px; margin-top: -10px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 20px;">
用户登录
</div>
<el-form :model="form" status-icon :rules="rules" ref="formRef" label-width="70px">
<el-form-item label="账号:" prop="phone" style="margin-bottom: 20px !important;">
<el-input v-model="form.phone" placeholder="请输入11位手机号码"></el-input>
</el-form-item>
<el-form-item label="密码:" prop="password" style="margin-bottom: 20px !important;">
<el-input type="password" v-model="form.password" placeholder="请输入账号密码"></el-input>
</el-form-item>
<div style="display: flex; justify-content: space-between;margin-left: 20px; margin-top: 10px; margin-bottom: 20px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<el-form-item>
<el-button style="position: relative; width: 45%; margin-left: -50px;margin-right:72px;" @click="navigateToHome">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">取消</span>
</el-button>
<el-button type="primary" style="position: relative; width: 45%; background-color: #dc99a1;" @click="submitForm">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">登录</span>
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { useUserStore } from '../../stores/index.ts';
const router = useRouter();
const userStore = useUserStore();
//
const form = reactive({
phone: '',
password: ''
});
const formRef = ref(null);
//
const rules = reactive({
phone: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的11位手机号码', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
]
});
//
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
// db.json
axios.get('http://localhost:3002/users')
.then(response => {
const users = response.data;
const user = users.find(u => u.phone === form.phone && u.password === form.password);
if (user) {
ElMessageBox.alert('登录成功', '提示', {
confirmButtonText: '确定',
callback: async () => {
try {
router.push('/user');
userStore.setLoginState(true);
} catch (error) {
console.error('Error setting login state:', error);
}
}
});
} else {
ElMessageBox.alert('账号或密码错误,请重新输入', '提示', {
confirmButtonText: '确定'
});
userStore.setLoginState(false);
console.log('登录状态已重置为:', userStore.isLoggedIn);
}
});
} else {
ElMessageBox.alert('请检查输入信息', '提示', {
confirmButtonText: '确定'
});
return false;
}
});
};
//
const navigateToHome = () => {
router.push('/');
};
//
const resetForm = () => {
formRef.value.resetFields();
};
//
const navigateToResetPassword = () => {
router.push('/reset-password');
};
</script>
<style scoped>
.login-container {
background-image: url('../public/背景1.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(3px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,123 +0,0 @@
<template>
<el-main class="logout-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 10px; margin-top: -10px;">
用户注销账号
</div>
<div style="text-align: center; font-size: 20px; color: red;margin-bottom: 10px;">
警告当前为注销操作
</div>
<div style="text-align: center; font-size: 20px; color: red;margin-bottom: 10px;">
如果您注销了该账号则该账号下绑定的一切信息都会丢失且不可恢复
</div>
<el-form :model="form" status-icon :rules="rules" ref="formRef" label-width="82px" label-position="left">
<el-form-item label="用户账号:" prop="account">
<el-input v-model="form.account" placeholder="请输入账号名/手机号/邮箱"></el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input type="password" v-model="form.password" placeholder="请输入密码"></el-input>
</el-form-item>
<div style="display: flex; justify-content: space-between; margin-top: 40px; margin-bottom: 10px;">
<el-link type="primary" @click="confirmLogout"></el-link>
<el-link type="primary" @click="cancel"></el-link>
</div>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const form = reactive({
account: '',
password: ''
});
const formRef = ref(null);
//
const validatePassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入密码'));
} else {
callback();
}
};
const rules = reactive({
account: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ validator: validatePassword, trigger: 'blur' }
]
});
const confirmLogout = async () => {
const valid = await new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
resolve(true);
} else {
resolve(false);
}
});
});
if (valid) {
try {
const users = await axios.get('http://localhost:3001/users');
const user = users.data.find(user => user.account === form.account && user.password === form.password);
if (!user) {
ElMessage.error('账号或密码错误');
return;
}
// DELETE
await axios.delete(`http://localhost:3001/users/${user.id}`);
ElMessage.success('账号已成功注销');
router.push('/');
formRef.value.resetFields();
} catch (error) {
ElMessage.error(`注销账号失败: ${error.message}`);
}
} else {
ElMessage.error('注销账号失败,请检查输入信息');
}
};
const cancel = () => {
formRef.value.resetFields();
router.push('/user/myInfo');
};
</script>
<style scoped>
.logout-container {
background-image: url('../public/大背景.jpeg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,357 +0,0 @@
<template>
<el-main class="register-container">
<el-card class="blur-card" style="width: 450px;">
<div style="text-align: center; font-size: 40px; margin-bottom: 20px; margin-top: -10px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 20px;">
{{ step === 1 ? '用户注册- 基础信息' : '用户注册 - 安全问题' }}
</div>
<el-form :model="form" status-icon :rules="currentRules" ref="formRef" label-width="96px" label-position="left">
<!-- Step 1: 用户昵称密码确认密码手机号码个性签名出生日期 -->
<el-form-item v-if="step === 1" label="用户昵称:" prop="username">
<el-input v-model="form.username" placeholder="请输入用户昵称"></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="手机号码:" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号码"></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="用户密码:" prop="password">
<el-input type="password" v-model="form.password" placeholder="请输入账号密码"></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="确认密码:" prop="checkpassword">
<el-input type="password" v-model="form.checkpassword" placeholder="两次账号密码要一致"></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="个性签名:" prop="signature">
<el-input v-model="form.signature" placeholder="请输入个性签名"></el-input>
</el-form-item>
<el-form-item v-if="step === 1" label="出生日期:" prop="birthday">
<el-date-picker v-model="form.birthday" type="date" placeholder="请选择出生日期" style="width: 100%;"></el-date-picker>
</el-form-item>
<!-- Step 2: 安全问题 -->
<el-form-item v-if="step === 2" label="第一个问题:" prop="question1">
<el-select v-model="form.question1" placeholder="请选择问题">
<el-option label="您最新喜欢的歌曲是什么?" value="您最新喜欢的歌曲是什么?"></el-option>
<el-option label="您小学的名字是什么?" value="您小学的名字是什么?"></el-option>
<el-option label="您最喜欢的书是什么?" value="您最喜欢的书是什么?"></el-option>
<el-option label="您最喜欢的电影是什么?" value="您最喜欢的电影是什么?"></el-option>
<el-option label="您的第一只宠物叫什么名字?" value="您的第一只宠物叫什么名字?"></el-option>
<el-option label="您小时候最好的朋友叫什么名字?" value="您小时候最好的朋友叫什么名字?"></el-option>
<el-option label="您最不喜欢的运动是什么?" value="您最不喜欢的运动是什么?"></el-option>
<el-option label="您第一次旅行的目的地是哪里?" value="您第一次旅行的目的地是哪里?"></el-option>
<el-option label="您最喜欢的明星叫什么名字?" value="您最喜欢的明星叫什么名字?"></el-option>
<el-option label="您最喜欢的音乐家是谁?" value="您最喜欢的音乐家是谁?"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="step === 2" label="第一个答案:" prop="answer1">
<el-input v-model="form.answer1" placeholder="请回答第一个问题"></el-input>
</el-form-item>
<el-form-item v-if="step === 2" label="第二个问题:" prop="question2">
<el-select v-model="form.question2" placeholder="请选择问题">
<el-option label="您最喜欢的城市是哪里?" value="您最喜欢的城市是哪里?"></el-option>
<el-option label="您初中毕业的学校是哪所?" value="您初中毕业的学校是哪所?"></el-option>
<el-option label="您最难忘的一次旅行是去哪里?" value="您最难忘的一次旅行是去哪里?"></el-option>
<el-option label="您最喜欢的动物是什么?" value="您最喜欢的动物是什么?"></el-option>
<el-option label="您最喜欢的季节是什么?" value="您最喜欢的季节是什么?"></el-option>
<el-option label="您小时候最喜欢的玩具是什么?" value="您小时候最喜欢的玩具是什么?"></el-option>
<el-option label="您最尊敬的人是谁?" value="您最尊敬的人是谁?"></el-option>
<el-option label="您最擅长的科目是什么?" value="您最擅长的科目是什么?"></el-option>
<el-option label="您最喜欢的歌曲是什么?" value="您最喜欢的歌曲是什么?"></el-option>
<el-option label="您最喜欢的节日是什么?" value="您最喜欢的节日是什么?"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="step === 2" label="第二个答案:" prop="answer2">
<el-input v-model="form.answer2" placeholder="请回答第二个问题"></el-input>
</el-form-item>
<!-- 操作按钮 -->
<div v-if="step === 1" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<div v-if="step === 1" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-button style="position: relative; width: 35%;" @click="navigateToHome">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">取消</span>
</el-button>
<el-button type="primary" style="position: relative; width: 35%; background-color: #dc99a1;" @click="nextStep">
<span style="position: absolute; top: 50%; transform: translate(-42%, -50%); letter-spacing: 20px; white-space: nowrap;">下一步</span>
</el-button>
</div>
<div v-if="step === 2" style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 10px;">
<el-link type="primary" @click="navigateToResetPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<el-form-item v-if="step === 2">
<el-button style="position: relative; width: 45%; margin-left: -97px; margin-right: 116px;" @click="previousStep">
<span style="position: absolute; top: 50%; transform: translate(-42%, -50%); letter-spacing: 20px; white-space: nowrap;">上一步</span>
</el-button>
<el-button type="primary" style="position: relative; width: 45%; background-color: #dc99a1;" @click="submitForm">
<span style="position: absolute; top: 50%; transform: translate(-32%, -50%); letter-spacing: 40px; white-space: nowrap;">注册</span>
</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { reactive, ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const form = reactive({
username: '', //
phone: '', //
password: '',
checkpassword: '',
signature: '', //
birthday: null, //
question1: '',
answer1: '',
question2: '',
answer2: ''
});
const formRef = ref(null);
const step = ref(1);
//
const validatePasswordConfirm = (rule, value, callback) => {
if (value !== form.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};
//
const rules = reactive({
username: [
{ required: true, message: '请输入用户昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的11位手机号码', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
],
checkpassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: validatePasswordConfirm, trigger: 'blur' }
],
signature: [
{ required: false, message: '请输入个性签名', trigger: 'blur' }
],
birthday: [
{ type: 'date', message: '请选择出生日期', trigger: 'change' }
],
question1: [
{ required: true, message: '请选择第一个问题', trigger: 'change' }
],
answer1: [
{ required: true, message: '请回答第一个问题', trigger: 'blur' }
],
question2: [
{ required: true, message: '请选择第二个问题', trigger: 'change' }
],
answer2: [
{ required: true, message: '请回答第二个问题', trigger: 'blur' }
]
});
//
const currentRules = computed(() => {
if (step.value === 1) {
return {
username: rules.username,
password: rules.password,
checkpassword: rules.checkpassword,
phone: rules.phone,
signature: rules.signature,
birthday: rules.birthday
};
} else {
return {
question1: rules.question1,
answer1: rules.answer1,
question2: rules.question2,
answer2: rules.answer2
};
}
});
//
const nextStep = async () => {
const valid = await new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
resolve(true);
} else {
resolve(false);
}
});
});
if (valid) {
step.value = 2;
} else {
ElMessageBox.alert('请检查输入信息', '提示', {
confirmButtonText: '确定'
});
}
};
//
const previousStep = () => {
step.value = 1;
};
//
const questionMapping = {
'您最新喜欢的歌曲是什么?': 'favorite_song',
'您小学的名字是什么?': 'elementary_school',
'您最喜欢的书是什么?': 'favorite_book',
'您最喜欢的电影是什么?': 'favorite_movie',
'您的第一只宠物叫什么名字?': 'first_pet_name',
'您小时候最好的朋友叫什么名字?': 'best_friend_childhood',
'您最不喜欢的运动是什么?': 'least_favorite_sport',
'您第一次旅行的目的地是哪里?': 'first_trip_destination',
'您最喜欢的明星叫什么名字?': 'favorite_celebrity',
'您最喜欢的音乐家是谁?': 'favorite_musician',
'您初中毕业的学校是哪所?': 'middle_school',
'您最难忘的一次旅行是去哪里?': 'memorable_trip',
'您最喜欢的动物是什么?': 'favorite_animal',
'您最喜欢的季节是什么?': 'favorite_season',
'您小时候最喜欢的玩具是什么?': 'childhood_toy',
'您最尊敬的人是谁?': 'most_respected_person',
'您最擅长的科目是什么?': 'best_subject',
'您最喜欢的歌曲是什么?': 'favorite_song',
'您最喜欢的节日是什么?': 'favorite_holiday',
'您最喜欢的城市是哪里?': 'favorite_city'
};
//
const submitForm = async () => {
const valid = await new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
resolve(true);
} else {
resolve(false);
}
});
});
if (valid) {
try {
//
const users = await axios.get('http://localhost:3002/users');
const existingUser = users.data.find(user => user.username === form.username); // username
if (existingUser) {
ElMessageBox.alert('该账号已存在,不能重复注册', '提示', {
confirmButtonText: '确定'
});
return;
}
// ID
const ids = users.data.map(user => user.id);
const maxId = Math.max(...ids.filter(id => id !== null), 0);
const nextId = maxId + 1;
//
const mappedQuestion1 = questionMapping[form.question1];
const mappedQuestion2 = questionMapping[form.question2];
const securityQuestions = [
{
question: form.question1,
question_key: mappedQuestion1,
answer: form.answer1
},
{
question: form.question2,
question_key: mappedQuestion2,
answer: form.answer2
}
];
//
await axios.post('http://localhost:3002/users', {
id: nextId.toString(),
username: form.username, // 使 username
password: form.password,
phone: form.phone,
signature: form.signature,
birthday: form.birthday,
security_questions: securityQuestions
});
ElMessageBox.alert('注册成功', '提示', {
confirmButtonText: '确定',
callback: () => {
resetForm();
router.push('/login');
}
});
} catch (error) {
if (error.response && error.response.status === 404) {
ElMessageBox.alert('模拟后端未正确启动或请求 URL 错误', '提示', {
confirmButtonText: '确定'
});
} else {
ElMessageBox.alert(`注册失败: ${error.message}`, '提示', {
confirmButtonText: '确定'
});
}
}
} else {
ElMessageBox.alert('注册失败,请检查输入信息', '提示', {
confirmButtonText: '确定'
});
}
};
//
const navigateToHome = () => {
router.push('/');
};
//
const resetForm = () => {
formRef.value.resetFields();
};
//
const navigateToResetPassword = () => {
router.push('/reset-password');
};
</script>
<style scoped>
.register-container {
background-image: url('../public/背景2.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px;
}
</style>

@ -1,173 +0,0 @@
<template>
<el-main class="USERlogin-container">
<el-card class="blur-card">
<div style="text-align: center; font-size: 40px; margin-bottom: 20px;">
简书微澜
</div>
<div style="text-align: center; font-size: 20px; margin-bottom: 20px;">
重置密码
</div>
<el-form :model="form" status-icon :rules="rules" ref="formRef" label-width="82px" label-position="left">
<el-form-item label="手机号码:" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号码"></el-input>
</el-form-item>
<el-form-item label="旧的密码:" prop="oldPassword">
<el-input type="password" v-model="form.oldPassword" placeholder="请输入旧密码"></el-input>
</el-form-item>
<el-form-item label="新的密码:" prop="newPassword">
<el-input type="password" v-model="form.newPassword" placeholder="请输入新密码"></el-input>
</el-form-item>
<el-form-item label="确认密码:" prop="confirmPassword">
<el-input type="password" v-model="form.confirmPassword" placeholder="请再次输入新密码"></el-input>
</el-form-item>
<div style="display: flex; justify-content: space-between; margin-top: 10px; margin-bottom: 20px;">
<el-link type="primary" @click="navigateToForgotPassword"></el-link>
<el-link type="primary" @click="resetForm"></el-link>
</div>
<el-form-item>
<el-button style="width: 45%; margin-left: -84px; margin-right: 54px;" @click="navigateToLogin"></el-button>
<el-button type="primary" style="width: 45%; margin-left: 52px; background-color: #dc99a1;" @click="submitForm"></el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
const form = reactive({
phone: '', // phone
oldPassword: '', //
newPassword: '',
confirmPassword: ''
});
const formRef = ref(null);
//
const validateConfirmPassword = (rule, value, callback) => {
if (value !== form.newPassword) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};
const rules = reactive({
phone: [ // phone
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
oldPassword: [ //
{ required: true, message: '请输入旧密码', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
});
const submitForm = async () => {
const valid = await new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
resolve(true);
} else {
resolve(false);
}
});
});
if (valid) {
try {
//
const users = await axios.get('http://localhost:3002/users');
const user = users.data.find(user => user.phone === form.phone && user.password === form.oldPassword); //
if (!user) {
ElMessageBox.alert('用户名或旧密码错误', '提示', {
confirmButtonText: '确定'
});
return;
}
//
const updatedUser = {
...user, //
password: form.newPassword //
};
// PUT
await axios.put(`http://localhost:3002/users/${user.id}`, updatedUser);
//
ElMessageBox.alert('密码重置成功', '提示', {
confirmButtonText: '确定',
callback: () => {
router.push('/login');
resetForm();
}
});
} catch (error) {
if (error.response && error.response.status === 404) {
ElMessageBox.alert('模拟后端未正确启动或请求 URL 错误', '提示', {
confirmButtonText: '确定'
});
} else {
ElMessageBox.alert(`密码重置失败: ${error.message}`, '提示', {
confirmButtonText: '确定'
});
}
}
} else {
ElMessageBox.alert('密码重置失败,请检查输入信息', '提示', {
confirmButtonText: '确定'
});
}
};
const navigateToLogin = () => {
router.push('/login');
};
const navigateToForgotPassword = () => {
router.push('/forgot-password');
};
const resetForm = () => {
formRef.value.resetFields();
};
</script>
<style scoped>
.USERlogin-container {
background-image: url('../public/背景3.jpg');
background-size: cover;
background-position: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.blur-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(3px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 40px!important;
width: 450px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.el-form-item {
margin-bottom: 15px!important;
}
</style>

@ -1,139 +0,0 @@
<template>
<el-container style="height: 100%;">
<!-- Header -->
<el-header class="header">
<el-row type="flex" justify="space-between" style="width:100%;" align="middle">
<!-- Logo -->
<el-col :span="6" class="logo-container">
<img src="@/assets/logo.png" alt="Logo" style="height: 120px;">
</el-col>
<!-- Logout Button -->
<el-col :span="6" style="text-align: right;">
<el-button type="danger" @click="logout">退</el-button>
</el-col>
</el-row>
</el-header>
<!-- Main Content -->
<el-container style="height: 80%;">
<!-- Aside (Left Sidebar) -->
<el-aside class="custom-aside">
<el-menu :default-active="activeMenu" :router="true" class="custom-menu">
<el-menu-item index="/">
<i class="el-icon-house"></i>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/profile1">
<i class="el-icon-user"></i>
<span>个人信息</span>
</el-menu-item>
<el-menu-item index="/edit">
<i class="el-icon-edit"></i>
<span>写文章</span>
</el-menu-item>
<el-menu-item index="/display">
<i class="el-icon-display"></i>
<span>文章列表</span>
</el-menu-item>
<el-menu-item index="/accounts">
<i class="el-icon-accounts"></i>
<span>账号安全</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- Main Section (Right Content) -->
<el-main>
<router-view />
</el-main>
</el-container>
<el-footer>
<span
style="height: 20px; position: fixed; bottom: 0; left: 0; right: 0; color: #333; text-align: center;">&copy;
2024 My Blog. All rights reserved.</span>
</el-footer>
</el-container>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user';
const activeMenu = ref('/profile1'); //
const router = useRouter();
const userStore = useUserStore();
// /profile1
onMounted(() => {
if (userStore.isAuthenticated) {
router.push('/profile1');
}
});
// 退
const logout = () => {
// userStore.logoutUser();
router.push("/");
};
</script>
<style scoped>
.header {
height: 100px;
background-color: #ffffff;
border-bottom: 1px solid #ddd;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
/* 确保元素分布 */
overflow: hidden;
}
.logo {
height: 120px;
object-fit: contain;
overflow: hidden;
/* 防止 logo 超出容器 */
}
.el-header {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
}
.custom-aside {
width: 220px;
height:100vh;
background-color: white; /* 更亮的背景色 */
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); /* 添加阴影 */
}
.custom-menu {
border: none;
}
.custom-menu .el-menu-item {
margin: 10px 0;
border-radius: 4px;
transition: background-color 0.3s, color 0.3s; /* 平滑的动画效果 */
}
.custom-menu .el-menu-item:hover {
background-color: #e6f7ff; /* 悬停背景 */
color: #1890ff; /* 悬停字体颜色 */
}
.custom-menu .el-menu-item.is-active {
background-color: #bae7ff; /* 激活状态背景 */
color: #096dd9; /* 激活状态字体颜色 */
}
</style>

@ -1,158 +0,0 @@
<template>
<el-container class="accounts">
<!-- 页面头部 -->
<el-header>
<h3 class="header-title">账户设置</h3>
</el-header>
<el-main>
<el-row justify="center" class="button-row">
<!-- 注销账号按钮 -->
<el-col :span="24" class="action-col">
<el-button type="danger" size="large" @click="showLogoutConfirm" class="action-btn">注销账号</el-button>
</el-col>
<!-- 修改密码按钮 -->
<el-col :span="24" class="action-col">
<el-button type="primary" size="large" @click="navigateToResetPassword" class="action-btn">修改密码</el-button>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script>
import { useRouter } from 'vue-router'; // Vue 3
export default {
setup() {
const router = useRouter(); // Vue 3
const showLogoutConfirm = () => {
ElMessageBox.confirm(
'您确定要注销您的账号吗?<br>此操作是不可逆的,并且会删除所有与该账号相关联的数据。<br>请谨慎操作。',
'警告',
{
dangerouslyUseHTMLString: true, // HTML
confirmButtonText: '取消', //
cancelButtonText: '我已阅读并同意注销', //
type: 'warning',
center: true,
showClose: false, //
beforeClose: (action, instance, done) => {
if (action === 'cancel') { // action 'cancel'
//
console.log('用户确认了注销');
// API
//
setTimeout(() => {
done(); //
//
}, 1000);
} else {
done(); //
}
},
}
).catch(() => {
//
console.log('用户取消了注销');
});
};
const navigateToResetPassword = () => {
router.push('/reset-password');
};
return {
showLogoutConfirm,
navigateToResetPassword,
};
},
};
</script>
<style scoped>
.accounts {
padding: 30px;
background-color: #f5f7fa;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.el-header {
text-align: center;
font-size: 26px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.header-title {
color: #409EFF;
font-family: 'Helvetica', sans-serif;
}
.button-row {
margin-top: 20px;
text-align: center;
}
.action-col {
margin-bottom: 20px;
}
.action-btn {
width: 200px;
height: 50px;
font-size: 16px;
border-radius: 25px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.action-btn:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.custom-dialog .el-dialog__header {
background-color: #409EFF;
color: white;
}
.password-form .el-form-item {
margin-bottom: 20px;
}
.input-field {
border-radius: 25px;
font-size: 14px;
height: 40px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.el-form-item label {
font-size: 14px;
color: #555;
}
.dialog-footer {
text-align: right;
}
.dialog-footer .el-button {
border-radius: 20px;
}
.el-button--primary {
background-color: #409EFF;
border-color: #409EFF;
transition: background-color 0.3s ease;
}
.el-button--primary:hover {
background-color: #66b1ff;
border-color: #66b1ff;
}
</style>

@ -1,269 +0,0 @@
<template>
<div class="article-list">
<ul>
<li v-for="(article, index) in articles" :key="article.id">
<div class="article-item">
<span class="article-title">{{ article.title }}</span>
<div class="button-group">
<button class="delete-btn" @click="deleteArticle(index)"></button>
<button class="edit-btn" @click="editArticle(article, index)">修改</button>
</div>
</div>
</li>
</ul>
<!-- 遮罩层 -->
<div v-if="showEditArticle" class="overlay" @click.self="cancelEdit"></div>
<!-- 修改已有文章表单 -->
<div v-if="showEditArticle" class="write-article-form edit-article-form">
<div class="form-header">
<h2>修改已有文章</h2>
</div>
<form @submit.prevent="submitEditArticle" class="form-container">
<div class="form-field">
<label for="edit-title">标题</label>
<input type="text" id="edit-title" v-model="existingArticle.title" placeholder="修改标题" required />
</div>
<div class="form-field">
<label for="edit-author">作者</label>
<input type="text" id="edit-author" v-model="existingArticle.author" placeholder="修改作者" required />
</div>
<div class="form-field">
<label for="edit-content">内容</label>
<textarea id="edit-content" v-model="existingArticle.content" placeholder="修改内容" required></textarea>
</div>
<div class="form-actions">
<button type="submit">更新文章</button>
<button type="button" @click="cancelEdit"></button>
</div>
</form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
articles: [
{ id: 1, title: '文章一', author: '张三', content: '这是文章一的内容' },
{ id: 2, title: '文章二', author: '李四', content: '这是文章二的内容' },
{ id: 3, title: '文章三', author: '王五', content: '这是文章三的内容' }
],
showEditArticle: false,
existingArticle: {}
};
},
methods: {
deleteArticle(index) {
if (confirm('确定要删除这篇文章吗?')) {
this.articles.splice(index, 1);
}
},
editArticle(article, index) {
//
this.existingArticle = Object.assign({}, article, { originalIndex: index });
this.showEditArticle = true;
},
submitEditArticle() {
//
const index = this.existingArticle.originalIndex;
this.articles.splice(index, 1, this.existingArticle);
this.cancelEdit();
},
cancelEdit() {
//
this.showEditArticle = false;
this.existingArticle = {};
}
}
};
</script>
<style scoped>
/* 样式保持不变 */
.article-list ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.article-list li {
margin: 15px 0;
padding: 15px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.article-item {
display: flex;
justify-content: space-between;
width: 100%;
}
.article-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.button-group {
display: flex;
gap: 10px;
}
button {
padding: 8px 15px;
font-size: 14px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
button:hover {
transform: translateY(-2px);
}
.delete-btn {
background-color: #e74c3c;
color: white;
}
.delete-btn:hover {
background-color: #c0392b;
}
.edit-btn {
background-color: #3498db;
color: white;
}
.edit-btn:hover {
background-color: #2980b9;
}
/* 新增样式 */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.write-article-form.edit-article-form {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #f8f9fa;
padding: 47px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 800px;
max-height: 80vh; /* 最大高度 */
display: flex;
flex-direction: column;
justify-content: center; /* 确保表单内容垂直居中 */
align-items: center; /* 确保表单内容水平居中 */
z-index: 1000;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.form-header {
width: 100%;
text-align: center;
margin-bottom: 20px;
}
.form-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
padding: 0 20px; /* 内边距以防止内容紧贴边缘 */
flex-grow: 1; /* 允许表单容器扩展 */
justify-content: center; /* 表单内容垂直居中 */
align-items: stretch; /* 表单项宽度一致 */
}
.form-field {
display: flex;
flex-direction: column;
gap: 8px;
}
label {
font-size: 14px;
font-weight: 600;
color: #333;
}
input, textarea {
padding: 12px;
font-size: 16px;
border: 2px solid #ccc;
border-radius: 8px;
transition: border-color 0.3s ease;
width: 96%; /* 确保输入框和文本区域宽度一致 */
}
input:focus, textarea:focus {
border-color: #007bff;
outline: none;
}
textarea {
resize: vertical;
min-height: 150px;
}
button[type="submit"] {
background-color: #28a745;
color: white;
font-weight: bold;
padding: 15px 35px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
button[type="submit"]:hover {
background-color: #218838;
transform: translateY(-2px);
}
button[type="button"] {
background-color: #007bff;
color: white;
padding: 15px 35px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
button[type="button"]:hover {
background-color: #0056b3;
transform: translateY(-2px);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
</style>

@ -1,153 +0,0 @@
<template>
<div class="edit-container">
<!-- 写文章表单 -->
<div class="write-article-form">
<h2>写文章</h2>
<form @submit.prevent="submitNewArticle">
<div>
<label for="title">标题</label>
<input type="text" id="title" v-model="newArticle.title" placeholder="输入标题" required />
</div>
<div>
<label for="author">作者</label>
<input type="text" id="author" v-model="newArticle.author" placeholder="输入作者" required />
</div>
<div>
<label for="content">内容</label>
<textarea id="content" v-model="newArticle.content" placeholder="输入内容" required></textarea>
</div>
<button type="submit">发布文章</button>
<button type="button" @click="resetForm"></button>
</form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
newArticle: {
title: '',
author: '',
content: ''
}
};
},
methods: {
submitNewArticle() {
//
if (this.newArticle.title && this.newArticle.author && this.newArticle.content) {
console.log('发布文章:', this.newArticle);
alert('文章已发布!');
//
this.resetForm();
} else {
alert('请填写所有字段');
}
},
resetForm() {
//
this.newArticle.title = '';
this.newArticle.author = '';
this.newArticle.content = '';
}
}
};
</script>
<style scoped>
/* 样式保持不变 */
.edit-container {
padding: 20px;
max-width: 1300px;
margin: 0 auto;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
form {
display: flex;
flex-direction: column;
gap: 20px;
}
form div {
display: flex;
flex-direction: column;
gap: 8px;
}
label {
font-size: 14px;
font-weight: 600;
color: #333;
}
input, textarea {
padding: 12px;
font-size: 16px;
border: 2px solid #ccc;
border-radius: 8px;
transition: border-color 0.3s ease;
}
input:focus, textarea:focus {
border-color: #007bff;
outline: none;
}
textarea {
resize: vertical;
min-height: 150px;
}
button[type="submit"] {
background-color: #28a745;
color: white;
font-weight: bold;
padding: 15px 35px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
button[type="submit"]:hover {
background-color: #218838;
transform: translateY(-2px);
}
button[type="button"] {
background-color: #007bff;
color: white;
padding: 15px 35px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
button[type="button"]:hover {
background-color: #0056b3;
transform: translateY(-2px);
}
.write-article-form {
background-color: #f8f9fa;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
font-size: 24px;
color: #333;
margin-bottom: 20px;
}
</style>

@ -1,162 +0,0 @@
<template>
<div v-if="post" class="post-detail">
<h1 class="post-title">{{ post.title }}</h1>
<p class="post-meta">
<strong>作者</strong>{{ post.author }} | <strong>发布时间</strong>{{ post.date }}
</p>
<div class="post-content" v-html="post.content"></div>
<el-button type="primary" @click="goBack" class="back-button">返回</el-button>
</div>
<div v-else class="loading">
<p>加载中...</p>
</div>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue';
import {
useRoute,
useRouter
} from 'vue-router';
//
const route = useRoute();
const router = useRouter();
// API
const posts = [{
id: 1,
title: '如何使用 Vue 3 构建高效的前端应用',
author: '张三',
date: '2024-11-01',
content: '<p>本文将介绍如何使用 Vue 3 构建高效的前端应用,深入讲解组件化开发和性能优化技术。</p>',
},
{
id: 2,
title: '深入浅出 Vue 3 响应式系统',
author: '李四',
date: '2024-11-05',
content: '<p>本文将带你深入理解 Vue 3 的响应式系统,帮助你更好地掌握 Vue 的核心特性。</p>',
},
{
id: 3,
title: 'Vue 3 中的 Composition API 使用技巧',
author: '王五',
date: '2024-11-10',
content: '<p>本文将介绍 Vue 3 中的 Composition API 的一些实用技巧,帮助你更高效地开发应用。</p>',
},
{
id: 4,
title: 'Vue 3 性能优化实战',
author: '赵六',
date: '2024-11-12',
content: '<p>本文将分享 Vue 3 中的一些性能优化技巧,帮助你提高应用的性能。</p>',
},
{
id: 5,
title: '如何构建 Vue 3 + TypeScript 项目',
author: '孙七',
date: '2024-11-15',
content: '<p>本文将介绍如何使用 Vue 3 和 TypeScript 构建一个高效且类型安全的前端项目。</p>',
},
{
id: 6,
title: 'Vue 3 Router 动态路由实践',
author: '周八',
date: '2024-11-18',
content: '<p>本文将介绍如何在 Vue 3 中使用 Router 的动态路由功能,提升应用的灵活性和可扩展性。</p>',
},
{
id: 7,
title: 'Vue 3 状态管理Pinia vs Vuex',
author: '钱九',
date: '2024-11-20',
content: '<p>本文将对比 Vue 3 中的状态管理库 Pinia 和 Vuex帮助你选择最适合的方案。</p>',
},
{
id: 8,
title: '如何在 Vue 3 中使用 Vue CLI',
author: '孙十',
date: '2024-11-25',
content: '<p>本文将介绍如何在 Vue 3 中使用 Vue CLI 来构建和管理 Vue 项目。</p>',
},
{
id: 9,
title: '从 Vue 2 到 Vue 3升级指南',
author: '吴十一',
date: '2024-11-28',
content: '<p>本文将介绍从 Vue 2 升级到 Vue 3 的过程,帮助你顺利完成升级,并避免常见的错误。</p>',
},
];
const post = ref(null);
// id
const postId = route.params.id;
//
onMounted(() => {
const currentPost = posts.find(p => p.id === parseInt(postId)); // id
if (currentPost) {
post.value = currentPost;
}
});
//
const goBack = () => {
router.push('/'); //
};
</script>
<style scoped>
.post-detail {
max-width: 800px;
margin: 40px auto;
padding: 30px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
height: 100vh;
}
.post-title {
font-size: 2.5rem;
font-weight: bold;
color: #333;
margin-bottom: 20px;
text-align: center;
}
.post-meta {
font-size: 0.9rem;
color: #666;
margin-bottom: 20px;
text-align: center;
}
.post-content {
font-size: 1rem;
line-height: 1.8;
color: #444;
padding: 10px 0;
}
.post-content img {
max-width: 100%;
border-radius: 8px;
margin: 20px 0;
}
.back-button {
display: block;
margin: 30px auto 0;
padding: 10px 30px;
font-size: 1rem;
}
</style>

@ -1,9 +0,0 @@
<template>
这是writer页面
</template>
<script>
</script>
<style>
</style>

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

@ -1,22 +0,0 @@
{
"files": [],
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"auto-imports.d.ts",
"components.d.ts",
],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
],
}

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

@ -1,11 +0,0 @@
{
"extends": "./tsconfig.app.json",
"exclude": [],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"lib": [],
"types": ["node", "jsdom"]
}
}

@ -1,49 +0,0 @@
import { fileURLToPath, URL } from 'node:url';
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import vueDevTools from 'vite-plugin-vue-devtools';
import envCompatible from 'vite-plugin-env-compatible';
// Element-Plus 按需导入
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// 根据当前工作目录process.cwd())中的 `mode` 加载 .env 文件
const env = loadEnv(mode, process.cwd(), 'VITE_');
return {
plugins: [
vue(),
vueJsx(),
vueDevTools(),
envCompatible(), // 添加 vite-plugin-env-compatible 插件
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 5173, // 强制 Vite 使用 5173 端口
strictPort: true, // 如果 5173 端口被占用Vite 将不会自动选择其他端口,而是抛出错误
proxy: {
'/api/v1': {
target: env.VITE_SERVER_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v1/, ''), // 重写路径,去掉前缀
},
},
},
envDir: "env", // 自定义环境变量的目录
};
});

@ -1,14 +0,0 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save