添加文件

This commit is contained in:
Luthics 2022-12-27 18:47:17 +08:00
commit c7df07d1c5
28 changed files with 9204 additions and 0 deletions

28
.gitignore vendored Normal file
View File

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

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

40
README.md Normal file
View File

@ -0,0 +1,40 @@
# YiiReader
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) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## 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 [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.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
```

230
doc/API (1).md Normal file
View File

@ -0,0 +1,230 @@
[toc]
### 请求地址
## https://fe2kao.tiaozhan.com/api/
## 用户模块
### 用户注册 `POST ${base}/user/registry`
#### request
```json
{
"name": "string",
"password": "string"
}
{
"name": "Luthics",
"password": "wenwen"
}
```
#### response
```json
{
"success": true,
"data": {
"name": "string",
"id": "number"
}
}
{
"success": true,
"data": {
"name": "Luthics",
"id": 9
}
}
```
### 用户登陆 `POST ${base}/user`
#### request
```json
{
"name": "string",
"password": "string"
}
```
#### response
```json
{
"success": true,
"data": {
"name": "string",
"id": "number"
}
}
```
### 获取用户登录信息 `GET ${base}/user/state`
#### request
```json
```
#### response
```json
{
"success": true,
"data": {
"id": "number",
"name": "string"
}
}
```
### 用户修改信息 `PUT ${base}/user`
#### request
```json
{
"name": "string",
"password": "string"
}
```
#### response
```json
{
"success": true
}
```
### 用户登出 `DELETE ${base}/user`
#### request
```json
```
#### response
```json
{
"success": true
}
```
## 文章模块
### 获取文章列表 `GET ${base}/passagelist`
**注意这里使用的参数传递方式为query传参而非请求体!**
#### request
```json
{
"type":"string", // 可选"ch"||"en",参数为可选,若传递该参数则获取全部文章列表
"page": "number",
"limit": "number",
}
```
#### response
```json
{
"success": true,
"data": {
"total": "number",//实际符合条件总数
"passages": [
{
"id": "string",
"title": "string"
},
{
...
}
]
}
}
```
### 获取文章内容 `GET ${base}/passage/:id`
**注意这里使用的参数传递方式为params传参而非请求体!**
#### request
```json
{
"id":"number"
}
```
#### response
```json
{
"success":true,
"data": {
"id":"number",
"title":"string",
"content":["string", ...], //按照段落分成数组
"comments":[
{
"id":"number",
"paragraph":"number", //评论的段落
"user":{
"name":"string" //评论者用户名
},
"marked":"string", //标记的文字
"comment":"string", //用户的评论
"createAt":"string", //创建时间
"updatedAt":"string" //修改时间
},
...
]
}
}
```
### 发布一条评论 `POST ${base}/comment`
#### request
```json
{
"passageId":"number",
"paragraph":"number",
"marked":"string",
"comment":"string"
}
```
#### response
```json
{
"success":true,
"data":{
"id":"number"
}
}
```
### 修改一条评论 `PUT ${base}/comment`
#### request
```json
{
"id":"number",
"comment":"string"
}
```
#### response
```json
{
"success":true
}
```
### 删除一条评论 `DELETE ${base}/comment`
**注意这里使用的参数传递方式为params传参而非请求体!**
#### request
```json
{
"id":"number"
}
```
#### response
```json
{
"success":true
}
```
## 错误请求
### 所有类型的错误请求将返回一个包含错误信息的返回体
#### response
```json
{
"success": false,
"error":"string"
}
```

BIN
doc/fe2kao-2022-explain.pdf Normal file

Binary file not shown.

1
env.d.ts vendored Normal file
View File

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

13
index.html Normal file
View File

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

7144
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "yiireader",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"axios": "^1.2.1",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-cli": "^2.9.6",
"vue-cookies": "^1.8.2",
"vue-router": "^4.1.6",
"vuex": "^4.1.0"
},
"devDependencies": {
"@types/node": "^18.11.12",
"@vicons/antd": "^0.12.0",
"@vicons/carbon": "^0.12.0",
"@vicons/fa": "^0.12.0",
"@vicons/fluent": "^0.12.0",
"@vicons/ionicons4": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/tsconfig": "^0.1.3",
"naive-ui": "^2.34.2",
"npm-run-all": "^4.1.5",
"typescript": "~4.7.4",
"vfonts": "^0.0.3",
"vite": "^4.0.0",
"vue-tsc": "^1.0.12"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

48
src/App.vue Normal file
View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { zhCN, dateZhCN } from 'naive-ui'
import { NConfigProvider, darkTheme, NMessageProvider,NLoadingBarProvider } from 'naive-ui'
import { useRouter, useRoute } from 'vue-router'
import HeaderView from './views/HeaderView.vue'
const router = useRouter()
</script>
<template>
<n-config-provider :locale="zhCN" :date-locale="dateZhCN" :theme="darkTheme" :class='{
top: router.currentRoute.value.path != "/login" && router.currentRoute.value.path != "/edit"
}'>
<n-message-provider>
<n-loading-bar-provider>
<HeaderView></HeaderView>
<RouterView></RouterView>
</n-loading-bar-provider>
</n-message-provider>
</n-config-provider>
<!-- <div class="left">
<router-view name="left"></router-view>
</div>
<CenterCol />
<div class="right">
<router-view name="right"></router-view>
</div> -->
</template>
<style scoped>
.top {
align-self: flex-start;
}
/* .right {
display: flex;
flex-direction: column;
padding-left: calc(var(--section-gap) / 2);
} */
</style>

74
src/assets/base.css Normal file
View File

@ -0,0 +1,74 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

38
src/assets/main.css Normal file
View File

@ -0,0 +1,38 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
align-items: center;
justify-content: flex-start;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: flex;
padding: 0 2rem;
height: 100vh;
}
}

83
src/components/Center.vue Normal file
View File

@ -0,0 +1,83 @@
<script setup lang="ts">
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<div class="col">
<div class="item">
<i>
<CommunityIcon />
</i>
</div>
<div class="item">
<i>
<SupportIcon />
</i>
</div>
</div>
</template>
<style>
.col {
display: flex;
flex-direction: column;
width: 0vw;
height: 100vh;
position: absolute;
left: 50%;
justify-content:space-around;
}
i {
display: flex;
place-items: center;
place-content: center;
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;
color: var(--color-text);
}
.item {
display: flex;
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
@media (min-width: 1024px) {
.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>

View File

@ -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>

View File

@ -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>

43
src/main.ts Normal file
View File

@ -0,0 +1,43 @@
import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'
import router from './router'
import VueCookies from 'vue-cookies'
import axios from 'axios'
import VueAxios from 'vue-axios'
import './assets/main.css'
import 'vfonts/Lato.css'
const app = createApp(App)
axios.defaults.baseURL = "/api"
const store = createStore({
state() {
return {
id: null,
name: null,
}
},
mutations: {
set(state, userInfo) {
state.id = userInfo.id
state.name = userInfo.name
},
clear(state) {
state.id = null
state.name = null
}
}
})
app.use(router)
app.use(VueCookies)
app.use(VueAxios, axios)
app.use(store)
app.mount('#app')

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

@ -0,0 +1,100 @@
import { createRouter, createWebHistory } from 'vue-router'
import VueCookies from 'vue-cookies'
import axios from 'axios'
import { useStore } from 'vuex'
const store = useStore()
const cookies = VueCookies
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/ListView.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue')
},
{
path: '/edit',
name: 'edit',
component: () => import('../views/EditView.vue')
},
{
path: '/404',
name: '404',
component: () => import('../views/404.vue')
},
{
path: '/:lang([ch|en|all]+)/:page(\\d+)',
name: 'list',
component: () => import('../views/ListView.vue')
},
{
path: '/page/:id(\\d+)',
name: 'page',
component: () => import('../views/ContentView.vue')
},
{
path: '/page/:id(\\d+)/:para(\\d+)',
name: 'comment',
component: () => import('../views/ContentView.vue')
},
{
path: '/:pathMatch(.*)*',
redirect: '/404'
}
]
})
function iscookieExist() {
if (!cookies.isKey('TENZOR_AUTH')) {
cookies.remove('TENZOR_AUTH');
}
cookies.set('TENZOR_AUTH', 'TEST');
if (!cookies.isKey('TENZOR_AUTH')) {
return true
}
else {
cookies.remove('TENZOR_AUTH')
return false
}
}
function issessionExist() {
if (sessionStorage.getItem('user_info') !== null) {
return true
}
else {
return false
}
}
// cookies.remove('TENZOR_AUTH');
// // cookies.set('TENZOR_AUTH', "o29P-qxkMkWoDCSC4cqiDQjxTV0_7u527PDckR-mWNFL6BHVM6KyJepcMjmiwjG-ieL44wWJdo2oKHSlFgJwwR1ij1LaYajAMs4Pp-fRaZb8=")
// console.log(cookies.get('TENZOR_AUTH'))
router.beforeEach(async (to, from) => {
// console.log(cookie)
// console.log(iscookieExist(),issessionExist())
if (iscookieExist()) { //有 cookie
if (to.name === 'login') {
return { name: 'home' }
}
}
else {
if (issessionExist()) {
sessionStorage.removeItem('user_info')
}
if (to.name !== 'login') {
return { name: 'login' }
}
}
})
export default router

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

@ -0,0 +1,28 @@
<script setup lang="ts">
import { NGradientText } from 'naive-ui';
</script>
<template>
<div class="s404">
<n-gradient-text type="success">
404 NOT FOUND
</n-gradient-text>
</div>
</template>
<style>
.n-gradient-text {
display: flex;
font-size: 3em;
}
.s404{
margin-top: calc(50vh - 1.5em);
display: flex;
justify-content: center;
align-content: center;
flex-wrap: wrap;
}
</style>

517
src/views/ContentView.vue Normal file
View File

@ -0,0 +1,517 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { NGradientText, NIcon, NButton, NTime, useMessage, useLoadingBar, NPopover, NModal, NCard, NInput } from 'naive-ui'
import { Comment28Regular as TrainIcon } from '@vicons/fluent'
import { Send as SendIcon } from '@vicons/carbon'
import { UpdateRound as UpdateIcon } from '@vicons/material'
import { ArrowBack as BackIcon } from '@vicons/ionicons5'
import { useStore } from 'vuex'
import axios from 'axios';
import { unset } from 'lodash'
const loadingbar = useLoadingBar()
const store = useStore()
const message = useMessage()
const route = useRoute()
const router = useRouter()
interface Page {
page: number | null
para: number | null
}
const pageInfo = reactive({
page: null,
para: null,
} as Page)
const contentInfo = reactive({
title: null,
paras: [] as string[],
comments: [] as comment[],
commentCount: [] as number[],
commentpara: [] ,
editing: null
} as ContentInfo)
interface ContentInfo {
title: string | null
paras: string[]
comments: comment[]
commentCount: number[]
commentpara: comment[]
editing: null
}
const JSONHeader = {
'Content-Type': 'application/json'
}
interface comment {
id: number,
paragraph: number,
marked: string,
comment: string,
createdAt: string,
updatedAt: string,
user: {
name: string
},
// push: Function
length: number,
}
interface Tool {
show: boolean,
x: number,
y: number,
text: string | null,
para: number | null,
modal: boolean,
comment: string | null
}
var tool = reactive({
show: false,
x: 0,
y: 0,
text: null,
para: null,
modal: false,
comment: null
} as Tool)
let canSelect = reactive([] as boolean[])
function canSel(index: number) {
for (let i = 0; i < canSelect.length; i++) canSelect[i] = false;
canSelect[index] = true;
}
async function updateContent(id: number) {
loadingbar.start()
await axios({
method: 'get',
url: 'passage/' + id,
}).then((res) => {
if (res.status == 200) {
if (res.data.success) {
loadingbar.finish()
contentInfo.title = res.data.data.title
contentInfo.paras = res.data.data.content
contentInfo.paras.forEach(() => {
contentInfo.commentCount.push(0)
contentInfo.commentpara.push([] as unknown as comment)
});
contentInfo.commentCount.push(0)
contentInfo.commentpara.push([] as unknown as comment)
contentInfo.comments = res.data.data.comments
contentInfo.comments.forEach(element => {
contentInfo.commentCount[0]++
contentInfo.commentCount[element.paragraph]++
contentInfo.commentpara[0].push(element)
contentInfo.commentpara[element.paragraph].push(element)
});
}
else {
loadingbar.error()
router.push('/')
}
} else {
loadingbar.error()
router.push('/')
}
})
}
if (route.params.id !== undefined) {
pageInfo.page = parseInt(route.params.id as string)
updateContent(pageInfo.page)
}
if (route.params.para !== undefined) {
pageInfo.para = parseInt(route.params.para as string)
if (contentInfo.commentCount[pageInfo.para] == 0 || contentInfo.commentCount[pageInfo.para] == null) {
pageInfo.para = null
router.push('/page/' + pageInfo.page)
}
}
function onMouse(event: object) {
var sele = window.getSelection()
if (sele === null || sele.anchorNode === null || sele.anchorNode.parentElement === null) return
var p = sele.anchorNode.parentElement
if (sele.type != 'Range' || p.localName != 'p') {
tool.show = false
return
}
var text = sele.toString().split('\n')[0]
if (text.length != 0) {
tool.show = true
tool.x = p.getBoundingClientRect().left
tool.y = p.getBoundingClientRect().top
tool.para = parseInt(p.id.split('_')[1])
tool.text = text.trim()
tool.comment = null
}
else {
tool.show = false
}
}
function viewComment(para: number) {
pageInfo.para = para
router.push('/page/' + pageInfo.page + '/' + para)
}
function Back() {
if (pageInfo.para !== null) {
pageInfo.para = null
router.push('/page/' + pageInfo.page)
}
else {
router.push('/')
}
}
async function editComment(id: number) {
var text = document.getElementById(id.toString())?.innerText;
loadingbar.start()
await axios({
method: 'put',
url: 'comment',
data: {
id: id,
comment: text
},
headers: JSONHeader
}).then((res) => {
if (res.status == 200 && res.data.success) {
loadingbar.finish()
message.success("修改成功")
contentInfo.editing = null
} else {
loadingbar.error()
message.error("出错了,再试一次吧")
}
})
}
async function delComment(id: number, para: number) {
loadingbar.start()
await axios({
method: 'delete',
url: 'comment',
params: {
id: id,
}
}).then((res) => {
if (res.status == 200 && res.data.success) {
loadingbar.finish()
message.success("删除成功")
contentInfo.editing = null
contentInfo.commentCount[0]--
contentInfo.commentCount[para]--
var index_0, index_1
for (var i = 0; i < contentInfo.commentpara[0].length; i++) {
if (contentInfo.commentpara[0][i].id == id) {
index_0 = i
break
}
}
for (var i = 0; i < contentInfo.commentpara[para].length; i++) {
if (contentInfo.commentpara[para][i].id == id) {
index_1 = i
break
}
}
contentInfo.commentpara[0].splice(index_0, 1)
contentInfo.commentpara[para].splice(index_1, 1)
if (contentInfo.commentCount[para] == 0) {
pageInfo.para = null
router.push('/page/' + pageInfo.page)
}
} else {
loadingbar.error()
message.error("出错了,再试一次吧")
}
})
}
function handleUpdateShow(show: boolean) {
console.log(show)
}
function showCommentbar() {
tool.modal = true
}
function sendComment() {
loadingbar.start()
if (tool.comment === null || tool.comment.length == 0) {
loadingbar.error()
message.error("请输入内容")
}
else {
axios({
method: 'post',
url: 'comment',
headers: JSONHeader,
data: {
passageId: pageInfo.page,
paragraph: tool.para,
marked: tool.text,
comment: tool.comment
}
}).then((res) => {
if (res.status == 200 && res.data.success) {
contentInfo.commentCount[0]++
contentInfo.commentCount[tool.para]++
var d = new Date()
var commentElement = {
"id": res.data.data.id,
"paragraph": tool.para,
"marked": tool.text,
"comment": tool.comment,
"createdAt": d.toJSON(),
"updatedAt": d.toJSON(),
"user": {
"name": store.state.name
}
}
contentInfo.commentpara[0].push(commentElement)
contentInfo.commentpara[tool.para].push(commentElement)
loadingbar.finish()
tool.modal = false
tool.comment = null
console.log(contentInfo)
}
else {
loadingbar.error()
}
})
}
}
</script>
<template>
<div class="main">
<div class="titlediv">
<n-gradient-text type="success" class="title">
{{ contentInfo.title }}
</n-gradient-text>
<n-gradient-text type="warning" class="subtitle" v-if="pageInfo.para === 0">
全部评论
</n-gradient-text>
<n-gradient-text type="warning" class="subtitle" v-else-if="pageInfo.para !== null">
{{ pageInfo.para }} 段评论
</n-gradient-text>
<div>
<n-button @click="viewComment(0)" v-if="pageInfo.para !== 0">
<template #icon>
<n-icon>
<train-icon />
</n-icon>
</template>
全部评论
</n-button>
<n-button @click="Back" style="margin-left:10px">
<template #icon>
<n-icon>
<BackIcon />
</n-icon>
</template>
返回
</n-button>
</div>
</div>
<div class="content" v-if="pageInfo.para === null">
<n-popover placement="left-start" :show="tool.show" :x="tool.x" :y="tool.y" @update:show="handleUpdateShow"
trigger="manual">
<n-button type="primary" text @click="showCommentbar">评论</n-button>
</n-popover>
<n-modal v-model:show="tool.modal">
<n-card style="width: 600px" title="发表评论" :bordered="false" size="huge" role="dialog" aria-modal="true">
<div class="marked">{{ tool.text }}</div>
<n-input v-model:value="tool.comment" type="textarea" style="margin-top:10px" placeholder="输入你的评论..." />
<template #footer>
<div class="buts">
<n-button @click="tool.modal = false" secondary>取消</n-button>
<n-button @click="sendComment()" style="margin-left: 10px" secondary>发表</n-button>
</div>
</template>
</n-card>
</n-modal>
<p v-for="(item, index) in contentInfo.paras" @mousedown="canSel(index)" @mouseup="onMouse"
:class="{ canselect: canSelect[index] }" :id="'t_' + (index + 1)">
{{ item }}
<n-button text v-if="contentInfo.commentCount[index + 1]" @click="viewComment(index + 1)"
style="user-select: none;">
<template #icon>
<n-icon>
<train-icon />
</n-icon>
</template>
查看 {{ contentInfo.commentCount[index + 1] }} 条评论
</n-button>
</p>
</div>
<div class="comment" v-else>
<div v-for="(item, index) in contentInfo.commentpara[pageInfo.para]" class="card">
<div class="cleft">
<n-gradient-text type="info" class="uname">
{{ item.user.name }}
</n-gradient-text>
<div class="row">
<n-icon>
<SendIcon />
</n-icon>
<n-time :time="Date.parse(item.createdAt)" type="relative" />
</div>
<div class="row">
<n-icon>
<UpdateIcon />
</n-icon>
<n-time :time="Date.parse(item.updatedAt)" type="relative" />
</div>
</div>
<div class="cright">
<div class="marked">
{{ item.marked != ' ' ? item.marked : "TA 只选中了一个空格" }}
</div>
<div class="commentr" :id="item.id"
:contenteditable="item.user.name == store.state.name && contentInfo.editing == item.id ? true : false">
{{ item.comment }}
</div>
<div class="buts" v-if="item.user.name == store.state.name">
<n-button @click="editComment(item.id)" size="small" secondary
v-if="contentInfo.editing == item.id">确定</n-button>
<n-button @click="contentInfo.editing = item.id, message.info('点击原评论即可编辑')" size="small" secondary
v-else>修改</n-button>
<n-button @click="delComment(item.id, item.paragraph)" style="margin-left: 10px" size="small"
secondary>删除</n-button>
</div>
</div>
</div>
</div>
</div>
</template>
<style>
.float {
display: none;
position: absolute;
}
.buts {
display: flex;
justify-content: flex-end;
align-items: center;
}
.row {
display: flex;
justify-content: center;
align-items: center;
}
time {
margin-left: 5px;
}
.subtitle {
font-size: xx-large;
}
.titlediv {
display: flex;
justify-content: space-between;
align-items: center;
}
.commentr {
word-wrap: break-word;
font-size: large;
max-width: 100%;
}
.marked {
padding: 5px;
border-radius: 0.5em;
border: 1px dotted gray;
}
.uname {
font-size: 1.4rem;
}
.cright {
padding: 10px;
width: 100%;
max-width: 50vw;
}
.cleft {
width: 10vw;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
/* border-bottom: 1px solid hsla(160, 100%, 37%, 1); */
}
.card {
display: flex;
margin: 0 0 10px 0;
}
.comment {
display: flex;
height: 80vh;
flex-direction: column;
overflow-y: scroll;
}
.title {
font-weight: 400;
font-size: 2.6rem;
top: -10px;
}
p {
user-select: none;
font-size: 1.1rem;
margin-top: 1rem;
cursor: text;
}
.canselect {
user-select: text;
}
.content {
/* display: none; */
display: flex;
flex-direction: column;
overflow-y: scroll;
}
.comment::-webkit-scrollbar {
display: none;
}
.content::-webkit-scrollbar {
display: none;
}
.main {
display: flex;
flex-direction: column;
width: 60vw;
height: 95vh;
padding: 7vh 0 3vh 0;
}
</style>

137
src/views/EditView.vue Normal file
View File

@ -0,0 +1,137 @@
<script setup lang="ts">
import axios from 'axios'
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { NGradientText, NSpace, NInput, NButton, NAlert, useLoadingBar } from 'naive-ui'
import { ro } from 'date-fns/locale'
const store = useStore()
const router = useRouter()
const loadingbar = useLoadingBar()
var name = store.state.name
const JSONHeader = {
'Content-Type': 'application/json'
}
var userInfo_input = reactive({
name: store.state.name,
password: undefined
})
var errmsg = reactive({
success: true,
error: "你输错啦"
})
function editProfile() {
loadingbar.start()
if (userInfo_input.name === undefined || userInfo_input.name.length == 0) {
loadingbar.error()
errmsg.success = false
errmsg.error = "请填写用户名"
return;
}
if (userInfo_input.password === undefined || userInfo_input.password.length == 0) {
loadingbar.error()
errmsg.success = false
errmsg.error = "请填写密码"
return;
}
axios({
method: 'put',
url: 'user',
data: userInfo_input,
headers: JSONHeader
}).then((res) => {
if (res.status == 200) {
errmsg.success = res.data.success
if (!res.data.success) {
loadingbar.error()
errmsg.error = res.data.error
}
else {
loadingbar.finish()
var userInfo = JSON.parse(sessionStorage.getItem('user_info'))
var newuserInfo = {
id: userInfo.id,
name: userInfo_input.name
}
store.commit('set', newuserInfo)
sessionStorage.setItem('user_info', JSON.stringify(newuserInfo))
router.push('/')
}
}
})
}
</script>
<template>
<n-space class="flexdiv" vertical>
<n-gradient-text type="success">
修改信息 / {{ name }}
</n-gradient-text>
<n-alert v-if="!errmsg.success" type="error" class="erralert">
{{ errmsg.error }}
</n-alert>
<div class="row flexdiv">
<div class="r_label">用户名</div>
<n-input v-model:value="userInfo_input.name" type="text" placeholder="请输入你的用户名" />
</div>
<div class="row flexdiv">
<div class="r_label">新密码</div>
<n-input v-model:value="userInfo_input.password" type="password" show-password-on="mousedown"
placeholder="请输入你的密码" />
</div>
<div class="row buts flexdiv">
<n-button @click="editProfile">修改</n-button>
</div>
</n-space>
</template>
<style scoped>
.flexdiv {
display: flex;
align-items: center;
justify-content: center;
}
.erralert {
width: 30vw;
}
.n-alert-body {
flex-direction: column;
}
.n-gradient-text {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
.n-button {
width: 200px;
}
.row {
width: 30vw;
margin-top: 10px;
flex-direction: row;
align-items: center;
justify-content: center;
}
.r_label {
justify-content: flex-start;
width: 70px;
}
.buts {
justify-content: flex-end;
}
</style>

189
src/views/HeaderView.vue Normal file
View File

@ -0,0 +1,189 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import axios from 'axios'
import { useRouter, useRoute } from 'vue-router'
import { NDropdown, NButton, useMessage, NInput, NGradientText } from 'naive-ui';
import { useStore } from 'vuex'
const router = useRouter()
const store = useStore()
const message = useMessage()
var edit = reactive({
state: false,
userInfo: {
name: null,
password: null
}
})
if (sessionStorage.getItem('user_info') === null) {
axios({
method: 'get',
url: 'user/state'
}).then((res) => {
if (res.status == 200 && res.data.success) {
var userInfo = {
id: res.data.data.id,
name: res.data.data.name
}
store.commit('set', userInfo)
sessionStorage.setItem('user_info', JSON.stringify(userInfo))
}
})
}
else {
if (store.state.id === null) {
var userInfo = JSON.parse(sessionStorage.getItem('user_info'))
store.commit('set', userInfo)
edit.userInfo = userInfo
}
}
var options = ref([
{
label: '查看个人资料',
key: 'profile',
},
{
label: '编辑用户资料',
key: 'editProfile',
},
{
label: '退出登录',
key: 'logout',
}
])
const JSONHeader = {
'Content-Type': 'application/json'
}
function handleSelect(key: string | number) {
switch (key) {
case 'profile': {
message.info(
"ID: " + store.state.id + " 丨 姓名:" + store.state.name,
{
keepAliveOnHover: true
}
)
break
}
case 'editProfile': {
router.push('/edit')
break
}
case 'logout': {
axios({
method: 'delete',
url: 'user'
}).then((res) => {
if (res.status == 200 && res.data.success) {
sessionStorage.removeItem('user_info')
store.commit('clear')
router.push('/login')
}
})
break
}
}
}
</script>
<template>
<div class="header" v-if='router.currentRoute.value.path != "/login"
&& router.currentRoute.value.path != "/edit"
&& router.currentRoute.value.path != "/404"'>
<n-dropdown :options="options" @select="handleSelect">
<n-button quaternary class="header_but" size="large">{{ store.state.name }}</n-button>
</n-dropdown>
</div>
<div class="editarea" v-if="edit.state">
<div class="buts flexdiv">
<n-gradient-text type="success">
修改信息
</n-gradient-text>
<div class="butts">
<n-button @click="edit.state = false">取消</n-button>
<n-button @click="editProfile">修改</n-button>
</div>
</div>
<div class="row flexdiv">
<div class="r_label">用户名</div>
<n-input v-model:value="edit.userInfo.name" type="text" placeholder="请输入你的用户名" />
</div>
<div class="row flexdiv">
<div class="r_label">密码</div>
<n-input v-model:value="edit.userInfo.password" type="password" show-password-on="mousedown"
placeholder="请输入你的密码" />
</div>
</div>
</template>
<style scoped>
.header {
display: flex;
position: relative;
flex-direction: row-reverse;
width: 100%;
height: 5vh;
}
.header_but {
justify-self: flex-end;
display: flex;
max-width: 100px;
margin-top: 10px;
border-bottom: 1px solid hsla(160, 100%, 37%, 1);
}
.editarea {
position: absolute;
margin-top: 30px;
border: 1px solid hsla(160, 100%, 37%, 1);
border-radius: 0.25rem;
padding: 10px 20px;
}
.flexdiv {
display: flex;
align-items: center;
justify-content: center;
}
.erralert {
width: 30vw;
}
.n-alert-body {
flex-direction: column;
}
.n-gradient-text {
font-weight: 500;
font-size: 1.4rem;
}
.n-button {
width: 100px;
}
.row {
margin-top: 10px;
flex-direction: row;
align-items: center;
justify-content: center;
}
.r_label {
justify-content: flex-start;
width: 70px;
}
.buts {
justify-content: space-between;
}
</style>

208
src/views/ListView.vue Normal file
View File

@ -0,0 +1,208 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { NGradientText, NPagination, NButton, useLoadingBar } from 'naive-ui'
import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router'
import axios from 'axios';
const store = useStore()
const router = useRouter()
const route = useRoute()
const loadingbar = useLoadingBar()
var contents: object = reactive([])
var pagedata = reactive({
page: 1,
total: 10,
lang: 'ch'
})
async function updateContent() {
var type = pagedata.lang == 'all' ? null : pagedata.lang;
loadingbar.start()
await axios({
method: 'get',
url: 'passagelist',
params: {
type: type,
page: pagedata.page,
limit: 9,
}
}).then((res) => {
if (res.status == 200 && res.data.success) {
loadingbar.finish()
contents.length = 0
res.data.data.passages.forEach(element => {
contents.push(element)
});
pagedata.total = Math.ceil(res.data.data.total / 9)
}
else {
loadingbar.error()
}
})
}
if (route.params.page !== undefined) {
pagedata.lang = route.params.lang
pagedata.page = parseInt(route.params.page)
}
updateContent()
function changeCH() {
var page = {
lang: pagedata.lang,
page: pagedata.page,
}
switch (pagedata.lang) {
case 'ch': {
page.lang = 'en'
pagedata.lang = 'en'
router.push({ name: 'list', params: page })
break
}
case 'en': {
page.lang = 'all'
pagedata.lang = 'all'
router.push({ name: 'list', params: page })
break
}
case 'all': {
page.lang = 'en'
pagedata.lang = 'en'
router.push({ name: 'list', params: page })
break
}
}
updateContent()
}
function changeEN() {
var page = {
lang: pagedata.lang,
page: pagedata.page,
}
switch (pagedata.lang) {
case 'ch': {
page.lang = 'all'
pagedata.lang = 'all'
router.push({ name: 'list', params: page })
break
}
case 'en': {
page.lang = 'ch'
pagedata.lang = 'ch'
router.push({ name: 'list', params: page })
break
}
case 'all': {
page.lang = 'ch'
pagedata.lang = 'ch'
router.push({ name: 'list', params: page })
break
}
}
updateContent()
}
</script>
<template>
<div class="cards_container flexdiv">
<div class="title">
<n-gradient-text type="success">
文章列表
</n-gradient-text>
<n-button class="langbut" :type="pagedata.lang == 'ch' || pagedata.lang == 'all' ? 'primary' : 'default'"
@click="changeCH">
</n-button>
<n-button class="langbut" :type="pagedata.lang == 'en' || pagedata.lang == 'all' ? 'primary' : 'default'"
@click="changeEN">
</n-button>
</div>
<div class="cards">
<router-link class="card" v-for="(item, index) in contents" :to="{ path: '/page/' + item.id }">
{{ item.title }}
</router-link>
</div>
<!-- pagedata.total -->
</div>
<div class="footer">
<n-pagination v-model:page="pagedata.page" :page-count="pagedata.total"
@update-page="updateContent(pagedata.lang, pagedata.page)" />
</div>
</template>
<style scoped>
.langbut {
margin-left: 30px;
font-size: large;
}
.title {
display: flex;
align-items: center;
}
.footer {
width: 100%;
position: absolute;
margin: 0 auto;
top: 95vh;
}
.n-pagination {
justify-content: center;
}
.n-gradient-text {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
.card {
display: flex;
border: 1px solid hsla(160, 100%, 37%, 1);
border-radius: 0.25rem;
flex-grow: 1;
width: 50vw;
padding: 0.5rem 1rem;
margin: 0.5rem 0;
font-size: 1.2rem;
}
.cards {
max-height: 75vh;
align-items: center;
display: flex;
flex-direction: column;
width: 51vw;
overflow-y: scroll;
}
.cards::-webkit-scrollbar {
display: none;
}
.cards_container {
display: flex;
flex-direction: column;
padding: 7vh 0 3vh 0;
}
.cards_container h1,
.cards_container h3 {
text-align: center;
}
@media (min-width: 1024px) {
.cards_container h1,
.cards_container h3 {
text-align: left;
}
}
</style>

177
src/views/LoginView.vue Normal file
View File

@ -0,0 +1,177 @@
<script setup lang="ts">
import { NGradientText, NSpace, NInput, NButton, NAlert, useLoadingBar } from 'naive-ui'
import axios from 'axios'
import { reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
const store = useStore()
const loadingbar = useLoadingBar()
var userInfo_input = reactive({
name: undefined,
password: undefined
})
var errmsg = reactive({
success: true,
error: "你输错啦"
})
const JSONHeader = {
'Content-Type': 'application/json'
}
const router = useRouter()
async function register() {
loadingbar.start()
if (userInfo_input.name === undefined || userInfo_input.name.length == 0) {
loadingbar.error()
errmsg.success = false
errmsg.error = "请填写用户名"
return;
}
if (userInfo_input.password === undefined || userInfo_input.password.length == 0) {
loadingbar.error()
errmsg.success = false
errmsg.error = "请填写密码"
return;
}
await axios({
method: 'post',
url: 'user/registry',
data: userInfo_input,
headers: JSONHeader
}).then((res) => {
if (res.status == 200) {
errmsg.success = res.data.success
if (!res.data.success) {
loadingbar.error()
errmsg.error = res.data.error
}
else {
loadingbar.finish()
var userInfo = {
id: res.data.data.id,
name: res.data.data.name
}
store.commit('set', userInfo)
sessionStorage.setItem('user_info', JSON.stringify(userInfo))
router.push('/')
}
}
})
}
function login() {
loadingbar.start()
if (userInfo_input.name === undefined || userInfo_input.name.length == 0) {
loadingbar.error()
errmsg.success = false
errmsg.error = "请填写用户名"
return;
}
if (userInfo_input.password === undefined || userInfo_input.password.length == 0) {
loadingbar.error()
errmsg.success = false
errmsg.error = "请填写密码"
return;
}
axios({
method: 'post',
url: 'user',
data: userInfo_input,
headers: JSONHeader
}).then((res) => {
if (res.status == 200) {
errmsg.success = res.data.success
if (!res.data.success) {
loadingbar.error()
errmsg.error = res.data.error
}
else {
loadingbar.finish()
var userInfo = {
id: res.data.data.id,
name: res.data.data.name
}
store.commit('set', userInfo)
sessionStorage.setItem('user_info', JSON.stringify(userInfo))
router.push('/')
}
}
})
}
</script>
<template>
<n-space class="flexdiv" vertical>
<n-gradient-text type="success">
登录账户 / Login
</n-gradient-text>
<n-alert v-if="!errmsg.success" type="error" class="erralert">
{{ errmsg.error }}
</n-alert>
<div class="row flexdiv">
<div class="r_label">用户名</div>
<n-input v-model:value="userInfo_input.name" type="text" placeholder="请输入你的用户名" />
</div>
<div class="row flexdiv">
<div class="r_label">密码</div>
<n-input v-model:value="userInfo_input.password" type="password" show-password-on="mousedown"
placeholder="请输入你的密码" />
</div>
<div class="row buts flexdiv">
<n-button @click="register">注册</n-button>
<n-button @click="login">登录</n-button>
</div>
</n-space>
</template>
<style scoped>
.flexdiv {
display: flex;
align-items: center;
justify-content: center;
}
.erralert {
width: 30vw;
}
.n-alert-body {
flex-direction: column;
}
.n-gradient-text {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
.n-button {
width: 100px;
}
.row {
width: 30vw;
margin-top: 10px;
flex-direction: row;
align-items: center;
justify-content: center;
}
.r_label {
justify-content: flex-start;
width: 70px;
}
.buts {
justify-content: space-around;
}
</style>

8
tsconfig.config.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

24
vite.config.ts Normal file
View File

@ -0,0 +1,24 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 5173,
proxy: {
'/api': {
target: 'https://fe2kao.tiaozhan.com/api',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
}
},
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})