Compare commits

..

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

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

32
.gitignore vendored

@ -0,0 +1,32 @@
# 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

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

@ -1,2 +1,45 @@
# bloggingplatform # 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
```

23
auto-imports.d.ts vendored

@ -0,0 +1,23 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const ElAside: typeof import('element-plus/es')['ElAside']
const ElButton: typeof import('element-plus/es')['ElButton']
const ElCarousel: typeof import('element-plus/es')['ElCarousel']
const ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
const ElCol: typeof import('element-plus/es')['ElCol']
const ElContainer: typeof import('element-plus/es')['ElContainer']
const ElFooter: typeof import('element-plus/es')['ElFooter']
const ElHeader: typeof import('element-plus/es')['ElHeader']
const ElInput: typeof import('element-plus/es')['ElInput']
const ElMain: typeof import('element-plus/es')['ElMain']
const ElMenu: typeof import('element-plus/es')['ElMenu']
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElRow: typeof import('element-plus/es')['ElRow']
}

48
components.d.ts vendored

@ -0,0 +1,48 @@
/* 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']
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']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
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
env.d.ts vendored

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

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

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

@ -0,0 +1,25 @@
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,
]

@ -0,0 +1,13 @@
<!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>

7297
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,49 @@
{
"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.7",
"element-plus": "^2.8.7",
"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-vue-devtools": "^7.5.4",
"vitest": "^2.1.4",
"vue-tsc": "^2.1.10"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

@ -0,0 +1,41 @@
<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>

@ -0,0 +1,90 @@
<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>

@ -0,0 +1,87 @@
<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>

@ -0,0 +1,11 @@
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')
})
})

@ -0,0 +1,7 @@
<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>

@ -0,0 +1,7 @@
<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>

@ -0,0 +1,7 @@
<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>

@ -0,0 +1,7 @@
<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>

@ -0,0 +1,19 @@
<!-- 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>

@ -0,0 +1,19 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersistedstate from 'pinia-plugin-persistedstate';
import App from './App.vue'
import router from './router'
const pinia = createPinia();
pinia.use(piniaPersistedstate);
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(pinia);
app.mount('#app')

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

@ -0,0 +1,49 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Post from '@/views/posts/post.vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: '/',
component: HomeView,
},
{
path: '/post/:id', // 使用动态参数 :id
name: 'postDetail',
component: Post,
props: true, // 将路由参数传递给组件
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register.vue'),
},
{
path: '/admin',
name: 'Admin',
component: () => import('../views/AdminView.vue'),
children: [
{
path: '/profile',
name: 'Profile',
component: () => import('../views/ProfileView.vue'),
},
{
path: '/article',
name: 'Article',
component: () => import('../views/ArticleView.vue'),
},
]
},
],
})
export default router

@ -0,0 +1,103 @@
// src/stores/user.ts
import type { IUserInfo } from '@/model/model';
import { defineStore } from 'pinia';
export const useUserStore = defineStore('users', {
state: () => ({
users: JSON.parse(localStorage.getItem('users') || '[]') as IUserInfo[], // 从localStorage加载已注册用户
currentUser: null as IUserInfo | null, // 当前登录的用户
}),
actions: {
// 注册用户
registerUser(user :IUserInfo) {
// 检查用户名是否已存在
const userExists = this.users.some((u: IUserInfo) => u.username === user.username);
if (userExists) {
return "用户已存在"
}
// 检查是不是管理员
if (user.username === "admin") {
return "管理员账号,不可使用"
}
// 如果用户名不存在,将新用户保存到数组中
this.users.push(user);
// 更新到 localStorage
localStorage.setItem('users', JSON.stringify(this.users));
return "成功"
},
// 登录用户
loginUser(username: string, password: string) {
if (username === 'admin' && password === '123456') {
// 模拟管理员登录
this.currentUser = {
username: 'admin',
nickname: '管理员',
signature: '我是系统管理员',
phone: '',
birthday: '1999-01-01',
isAdmin: true, // 设置管理员标识
password: '123456',
};
return this.currentUser
}
// 查找匹配的用户
const user = this.users.find((user: IUserInfo) => user.username === username && user.password === password);
if (!user) {
return null
}
// 设置当前用户
this.currentUser = user;
return user;
},
// 退出登录
logoutUser() {
this.currentUser = null;
},
updateUser(updatedUserInfo: IUserInfo) {
// 如果当前用户不存在,返回
if (!this.currentUser) return;
console.log("currentuser = ", this.currentUser)
// 更新当前用户
this.currentUser = { ...this.currentUser, ...updatedUserInfo };
console.log("currentuser1 = ", this.currentUser)
// 更新 users 数组中的对应用户信息
const userIndex = this.users.findIndex((user: IUserInfo) => user.username === this.currentUser.username);
// 如果找到了对应的用户,更新该用户信息
if (userIndex !== -1) {
// 使用深拷贝来更新 user 数组中的用户信息
this.users[userIndex] = { ...this.users[userIndex], ...updatedUserInfo };
// 更新 users 数组到 localStorage
localStorage.setItem('users', JSON.stringify(this.users));
}
},
// 获取当前用户信息
getCurrentUser() {
return this.currentUser;
},
// 设置管理员登录
setAdmin() {
}
},
persist: true, // 启用持久化
});

@ -0,0 +1,128 @@
<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';
const activeMenu = ref('/profile');
const router = useRouter();
const userStore = useUserStore();
onMounted(() => {
router.push(activeMenu.value)
})
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>

@ -0,0 +1,74 @@
<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 prop="status" label="状态" width="120">
<template v-slot="scope">
<el-tag :type="scope.row.status === '审核通过' ? 'success' : 'warning'">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<!-- 操作按钮 -->
<el-table-column label="操作" >
<template v-slot="scope">
<el-button @click="auditArticle(scope.row)" type="primary">审核</el-button>
<el-button @click="deleteArticle(scope.row)" type="danger">删除</el-button>
<el-button @click="stickArticle(scope.row)" type="info">置顶</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
//
const articles = ref([
{ id: 1, title: 'Vue 3入门', author: 'John Doe', status: '待审核' },
{ id: 2, title: 'JavaScript基础', author: 'Jane Doe', status: '审核通过' },
{ id: 3, title: '前端工程化', author: 'Alice Smith', status: '待审核' },
]);
//
const auditArticle = (article: any) => {
//
console.log('审核文章:', article.title);
article.status = '审核通过'; //
};
//
const deleteArticle = (article: any) => {
//
const index = articles.value.indexOf(article);
if (index !== -1) {
articles.value.splice(index, 1); //
}
};
//
const stickArticle = (article: any) => {
//
console.log('置顶文章:', article.title);
article.status = '置顶'; //
};
</script>
<style scoped>
.el-header {
padding: 0;
}
.el-button {
margin-left: 10px;
}
</style>

@ -0,0 +1,471 @@
<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="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, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user';
import { IUserInfo } from '../model/model';
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 posts = ref([
{ id: 1, title: 'Vue 3 入门教程', author: 'John Doe', date: '2024-11-29', excerpt: '这是一篇关于 Vue 3 的入门教程,适合初学者阅读。' },
{ id: 2, title: 'Vue 3 响应式系统解析', author: 'Jane Doe', date: '2024-11-28', excerpt: '深入浅出地解析 Vue 3 中的响应式系统。' },
{ id: 3, title: 'Vue 3 性能优化技巧', author: 'Alice Smith', date: '2024-11-27', excerpt: '如何在 Vue 3 中实现性能优化,提升应用效率。' },
{ id: 4, title: 'Vue 3 Composition API 实战', author: 'Bob Brown', date: '2024-11-26', excerpt: '详细讲解 Vue 3 的 Composition API 并通过案例演示。' },
{ id: 5, title: 'Vue 3 路由详解', author: 'Carol White', date: '2024-11-25', excerpt: '讲解如何在 Vue 3 中配置和使用 Vue Router。' },
{ id: 6, title: 'Vue 3 状态管理Pinia vs Vuex', author: 'Dave Black', date: '2024-11-24', excerpt: '比较 Vue 3 中的 Pinia 和 Vuex帮助你选择最适合的状态管理工具。' },
{ id: 7, title: 'Vue 3 与 TypeScript 的完美结合', author: 'Eve Green', date: '2024-11-23', excerpt: '教你如何在 Vue 3 中使用 TypeScript 开发应用。' },
{ id: 8, title: 'Vue 3 表单验证最佳实践', author: 'Frank Blue', date: '2024-11-22', excerpt: '介绍如何在 Vue 3 中进行表单验证,提升用户体验。' },
{ id: 9, title: 'Vue 3 与 Webpack 配置优化', author: 'Grace Purple', date: '2024-11-21', excerpt: '讲解如何优化 Vue 3 与 Webpack 配合使用时的配置。' }
]);
const latestPosts = ref([
{ id: 1, title: '如何使用 Vue 3 构建高效的前端应用' },
{ id: 2, title: '深入浅出 Vue 3 响应式系统' },
{ id: 3, title: 'Vue 3 中的 Composition API 使用技巧' },
]);
const popularPosts = ref([
{ id: 4, title: 'Vue 3 性能优化实战' },
{ id: 5, title: '如何构建 Vue 3 + TypeScript 项目' },
{ id: 6, title: 'Vue 3 Router 动态路由实践' },
]);
const recommendedPosts = ref([
{ id: 7, title: 'Vue 3 状态管理Pinia vs Vuex' },
{ id: 8, title: '如何在 Vue 3 中使用 Vue CLI' },
{ id: 9, title: '从 Vue 2 到 Vue 3升级指南' },
]);
const router = useRouter();
const userStore = useUserStore();
const currentUser = reactive<IUserInfo>({ username: '', nickname: '', signature: '', phone: '', birthday: '', isAdmin: false, password: '' });
const isLogin = ref(false);
const init = () => {
const user = userStore.getCurrentUser();
if (user) {
currentUser.username = user.username || '';
currentUser.nickname = user.nickname || '';
currentUser.signature = user.signature || '';
currentUser.phone = user.phone || '';
currentUser.birthday = user.birthday || '';
currentUser.isAdmin = user.isAdmin || false;
currentUser.password = user.password || '';
isLogin.value = true;
} else {
isLogin.value = false;
}
};
onMounted(() => {
init();
});
const handleLogin = () => {
router.push({ "name": "Login" });
};
const handleRegister = () => {
router.push("/register");
};
const handleLogout = () => {
userStore.logoutUser();
init();
router.push("/");
};
const gotoHome = () => {
router.push("/admin");
};
</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>

@ -0,0 +1,179 @@
<template>
<el-container class="login">
<el-main class="main-content">
<el-tabs v-model="activeTab" class="tabs" style="width: 100%;">
<el-tab-pane label="管理员登录" name="first">
<el-form :model="adminForm" :rules="adminRules" ref="adminFormRef" label-position="top" status-icon
class="form-container">
<el-form-item label="用户名" prop="username">
<el-input v-model="adminForm.username" placeholder="请输入管理员用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="adminForm.password" placeholder="请输入管理员密码" />
</el-form-item>
<el-button type="primary" @click="adminLogin" class="login-btn">登录</el-button>
</el-form>
</el-tab-pane>
<el-tab-pane label="用户登录" name="second">
<el-form :model="userForm" :rules="userRules" ref="userFormRef" label-position="top" status-icon
class="form-container">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="userForm.password" placeholder="请输入密码" />
</el-form-item>
<el-button type="primary" @click="userLogin" class="login-btn">登录</el-button>
<el-button type="text" @click="toggleRegister" class="register-btn">注册</el-button>
</el-form>
</el-tab-pane>
</el-tabs>
</el-main>
</el-container>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user';
//
const activeTab = ref<string>('first');
//
const adminForm = ref({
username: '',
password: '',
});
const adminRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
//
const userForm = ref({
username: '',
password: '',
});
const userRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
const router = useRouter();
const userStore = useUserStore();
//
const adminLogin = () => {
console.log('管理员登录:', adminForm.value);
if (adminForm.value.username == "admin" && adminForm.value.password == "123456") {
const u = userStore.loginUser(adminForm.value.username, adminForm.value.password);
if (u != null) {
router.push("/admin");
} else {
ElMessage.error('登录失败,请检查用户名和密码');
}
} else {
ElMessage.error('登录失败,请检查用户名和密码');
}
};
//
const userLogin = () => {
console.log('用户登录:', userForm.value);
const u = userStore.loginUser(userForm.value.username, userForm.value.password);
if (u != null) {
router.push("/admin");
} else {
ElMessage.error('登录失败,请检查用户名和密码');
}
};
//
const toggleRegister = () => {
router.push("/register");
};
</script>
<style scoped>
.login {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-image: url('../assets/bg.jpg');
background-repeat: no-repeat;
/* 不重复背景图片 */
background-size: cover;
/* 使背景图片覆盖整个页面 */
background-position: center center;
}
.main-content {
width: 100%;
max-width: 450px;
/* 控制最大宽度 */
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.tabs {
width: 100%;
}
.form-container {
width: 100%;
}
.el-tabs__header {
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.el-tabs__item {
font-size: 16px;
font-weight: 500;
}
.el-input {
border-radius: 6px;
padding: 10px;
}
.el-form-item {
margin-bottom: 20px;
}
.el-button {
border-radius: 6px;
font-size: 16px;
padding: 12px 0;
}
.login-btn {
background-color: #409EFF;
color: #fff;
}
.register-btn {
background-color: transparent;
color: #409EFF;
border: none;
margin-top: 10px;
}
.el-button:focus {
box-shadow: none;
}
.el-button:hover {
opacity: 0.9;
}
.el-message {
font-size: 14px;
}
</style>

@ -0,0 +1,138 @@
<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> {{ currentUser.isAdmin ? '管理员' : '普通用户' }}</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 v-if="!currentUser.isAdmin" class="user-item">
<strong>手机号码:</strong>
<input v-model="currentUser.phone" class="editable-input" type="text" />
</p>
<!-- 可编辑的生日 -->
<p class="user-item">
<strong>生日:</strong>
<input v-model="currentUser.birthday" class="editable-input" type="date" />
</p>
<p class="user-item">
<el-button type="primary" @click="saveUser" style="width: 100%;" v-if="!currentUser.isAdmin"></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';
// Piniauser store
const userStore = useUserStore();
const currentUser = reactive<IUserInfo>({
isAdmin: false,
nickname: '',
signature: '',
phone: '',
birthday: '',
username: '',
password: ''
});
onMounted(() => {
//
const user = userStore.getCurrentUser()
currentUser.username = user.username || '';
currentUser.nickname = user.nickname || '';
currentUser.signature = user.signature || '';
currentUser.phone = user.phone || '';
currentUser.birthday = user.birthday || '';
currentUser.isAdmin = user.isAdmin || false;
currentUser.password = user.password || '';
console.log("current = ", currentUser);
})
const saveUser = () => {
// currentUser
userStore.updateUser(currentUser); //
console.log('用户信息已更新:', currentUser);
ElMessage.success("保存成功")
}
</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;
}
</style>

@ -0,0 +1,191 @@
<template>
<el-container class="register">
<el-main class="main-content">
<span class="title">用户注册</span>
<el-form :model="userForm" :rules="userRules" ref="userFormRef" label-position="top" status-icon
class="form-container">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="userForm.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="userForm.confirmPassword" placeholder="请确认密码" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="userForm.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="个性签名" prop="signature">
<el-input v-model="userForm.signature" placeholder="请输入个性签名" />
</el-form-item>
<el-form-item label="生日" prop="birthday">
<input v-model="userForm.birthday" class="editable-input" type="date" />
</el-form-item>
<el-button type="primary" @click="submit(userFormRef)" class="register-btn">注册</el-button>
</el-form>
</el-main>
</el-container>
</template>
<script lang="ts" setup>
import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue';
import { useUserStore } from '../stores/user';
import { IUserInfo } from '../model/model';
import { useRouter } from 'vue-router';
const userStore = useUserStore();
const router = useRouter();
interface RuleForm {
username : string;
password : string;
confirmPassword : string;
birthday : string;
phone : string;
signature : string;
}
const userFormRef = ref<FormInstance>();
//
const userForm = reactive<RuleForm>({
username: '',
password: '',
confirmPassword: '',
birthday: '',
phone: '',
signature: '',
});
const validatePass2 = (rule : any, value : any, callback : any) => {
if (value === '') {
callback(new Error('Please input the password again'));
} else if (value !== userForm.password) {
callback(new Error("Two inputs don't match!"));
} else {
callback();
}
};
const userRules = reactive<FormRules<RuleForm>>({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
confirmPassword: [{ validator: validatePass2, trigger: 'blur', message: "密码不一致" }],
});
const submit = async (formEl : FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (valid) {
const user : IUserInfo = {
username: userForm.username,
password: userForm.password,
isAdmin: false,
birthday: userForm.birthday,
phone: userForm.phone,
signature: userForm.signature,
};
const msg = userStore.registerUser(user);
if (msg == '成功') {
ElMessage.success('注册成功');
router.push('/login');
} else {
ElMessage.error(msg);
}
} else {
ElMessage.error('表单验证失败,请检查输入');
return false;
}
});
};
</script>
<style scoped>
.register {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-image: url('../assets/bg.jpg');
background-repeat: no-repeat; /* 不重复背景图片 */
background-size: cover; /* 使背景图片覆盖整个页面 */
background-position: center center;
/* 页面背景色 */
}
.main-content {
width: 100%;
max-width: 450px;
/* 最大宽度 */
padding: 30px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
/* 添加阴影 */
}
.title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
/* 标题与表单之间的间距 */
color: #409eff;
/* 标题颜色 */
}
.el-form {
width: 100%;
}
.el-form-item {
margin-bottom: 20px;
}
.el-input {
border-radius: 6px;
padding: 10px;
}
.el-button {
font-size: 16px;
padding: 12px 0;
border-radius: 6px;
margin-top: 20px;
}
.register-btn {
background-color: #409eff;
color: #fff;
}
.el-button:focus {
box-shadow: none;
}
.el-button:hover {
opacity: 0.9;
}
/* 日期输入框样式 */
.editable-input {
width: 100%;
padding: 10px;
border-radius: 6px;
border: 1px solid #dcdfe6;
background-color: #fff;
}
.el-message {
font-size: 14px;
}
</style>

@ -0,0 +1,162 @@
<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>

@ -0,0 +1,16 @@
{
"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
}
}

@ -0,0 +1,22 @@
{
"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"
}
],
}

@ -0,0 +1,21 @@
{
"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"]
}
}

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

@ -0,0 +1,50 @@
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'
// 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 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有
// `VITE_` 前缀。
const env = loadEnv(mode, process.cwd()+"/env", 'VITE_')
// console.log("env = ", env)
return {
plugins: [
vue(),
vueJsx(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
// 处理跨域问题
server: {
port: 3000,
proxy: {
"/api/v1": {
target: env.VITE_SERVER_URL,
changeOrigin: true,
}
}
},
// 自定义环境变量的目录
envDir: "env"
}
})

@ -0,0 +1,14 @@
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)),
},
}),
)
Loading…
Cancel
Save