feat
This commit is contained in:
parent
aa900531b6
commit
0f6c1a0094
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
|
||||||
104
README.md
104
README.md
@ -0,0 +1,104 @@
|
|||||||
|
# 文本阅读器 - 用户端
|
||||||
|
|
||||||
|
这是一个基于 Vue 3 + TypeScript + Vite + Vant 构建的移动端文本阅读器应用。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 📱 响应式设计,自适应各种屏幕尺寸
|
||||||
|
- 📖 流畅的文本阅读体验
|
||||||
|
- 🔍 文件搜索功能
|
||||||
|
- 🎨 多种阅读主题(白色、护眼黄、绿色、灰色、夜间模式)
|
||||||
|
- 📏 可调节字体大小和行间距
|
||||||
|
- 💾 自动保存阅读设置
|
||||||
|
- ⚡ 下拉刷新
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- Vue 3
|
||||||
|
- TypeScript
|
||||||
|
- Vite
|
||||||
|
- Vant 4(移动端组件库)
|
||||||
|
- Vue Router
|
||||||
|
- Pinia(状态管理)
|
||||||
|
- Axios
|
||||||
|
- Sass
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# 或
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览生产构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/ # API 接口
|
||||||
|
│ ├── request.ts # axios 封装
|
||||||
|
│ └── reader.ts # 阅读器相关接口
|
||||||
|
├── router/ # 路由配置
|
||||||
|
├── styles/ # 全局样式
|
||||||
|
├── views/ # 页面组件
|
||||||
|
│ ├── Home.vue # 首页(文件列表)
|
||||||
|
│ └── Reader.vue # 阅读页面
|
||||||
|
├── App.vue # 根组件
|
||||||
|
└── main.ts # 入口文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### API 代理配置
|
||||||
|
|
||||||
|
在 `vite.config.ts` 中配置了 API 代理:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:9220',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
请根据实际后端服务地址修改 `target`。
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
1. 在首页可以查看所有可用的文本文件
|
||||||
|
2. 点击文件卡片进入阅读页面
|
||||||
|
3. 在阅读页面点击右上角设置图标可以调整阅读设置
|
||||||
|
4. 支持下拉刷新文件列表
|
||||||
|
5. 支持搜索文件名或描述
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 确保后端服务已启动并运行在正确的端口
|
||||||
|
- 建议使用现代浏览器以获得最佳体验
|
||||||
|
- 移动端访问时建议添加到主屏幕以获得类似原生应用的体验
|
||||||
|
|
||||||
|
|
||||||
22
components.d.ts
vendored
Normal file
22
components.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
VanButton: typeof import('vant/es')['Button']
|
||||||
|
VanEmpty: typeof import('vant/es')['Empty']
|
||||||
|
VanIcon: typeof import('vant/es')['Icon']
|
||||||
|
VanLoading: typeof import('vant/es')['Loading']
|
||||||
|
VanNavBar: typeof import('vant/es')['NavBar']
|
||||||
|
VanPopup: typeof import('vant/es')['Popup']
|
||||||
|
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
||||||
|
VanSearch: typeof import('vant/es')['Search']
|
||||||
|
VanSlider: typeof import('vant/es')['Slider']
|
||||||
|
}
|
||||||
|
}
|
||||||
15
index.html
Normal file
15
index.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>文本阅读器</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "agileboot-reader-mobile",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.2.5",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"vant": "^4.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vue-tsc": "^1.8.27",
|
||||||
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"unplugin-auto-import": "^0.17.2",
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"sass": "^1.69.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
1835
pnpm-lock.yaml
generated
Normal file
1835
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
src/App.vue
Normal file
23
src/App.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// App组件
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
54
src/api/reader.ts
Normal file
54
src/api/reader.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { request } from './request'
|
||||||
|
|
||||||
|
export interface TextFile {
|
||||||
|
fileId: number
|
||||||
|
fileName: string
|
||||||
|
originalFileName: string
|
||||||
|
filePath: string
|
||||||
|
fileSize: number
|
||||||
|
description: string
|
||||||
|
status: number
|
||||||
|
uploadUserId: number
|
||||||
|
uploadUserName: string
|
||||||
|
createTime: string
|
||||||
|
updateTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextFileContent {
|
||||||
|
fileId: number
|
||||||
|
fileName: string
|
||||||
|
content: string
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有可用文本文件列表
|
||||||
|
*/
|
||||||
|
export const getAllTextFiles = () => {
|
||||||
|
return request<TextFile[]>({
|
||||||
|
url: '/reader/public/files',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件详情
|
||||||
|
*/
|
||||||
|
export const getFileDetail = (fileId: number) => {
|
||||||
|
return request<TextFile>({
|
||||||
|
url: `/reader/public/file/${fileId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取文件内容
|
||||||
|
*/
|
||||||
|
export const readFileContent = (fileId: number) => {
|
||||||
|
return request<TextFileContent>({
|
||||||
|
url: `/reader/public/read/${fileId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
59
src/api/request.ts
Normal file
59
src/api/request.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { showToast, showLoadingToast, closeToast } from 'vant'
|
||||||
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
interface ResponseData<T = any> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const service: AxiosInstance = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse<ResponseData>) => {
|
||||||
|
const { code, data, msg } = response.data
|
||||||
|
|
||||||
|
if (code === 200 || code === 0) {
|
||||||
|
return response.data
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
message: msg || '请求失败',
|
||||||
|
type: 'fail'
|
||||||
|
})
|
||||||
|
return Promise.reject(new Error(msg || '请求失败'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
showToast({
|
||||||
|
message: error.message || '网络错误',
|
||||||
|
type: 'fail'
|
||||||
|
})
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default service
|
||||||
|
|
||||||
|
export const request = <T = any>(config: AxiosRequestConfig): Promise<ResponseData<T>> => {
|
||||||
|
return service.request<any, ResponseData<T>>(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
87
src/auto-imports.d.ts
vendored
Normal file
87
src/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const createPinia: typeof import('pinia')['createPinia']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const mapActions: typeof import('pinia')['mapActions']
|
||||||
|
const mapGetters: typeof import('pinia')['mapGetters']
|
||||||
|
const mapState: typeof import('pinia')['mapState']
|
||||||
|
const mapStores: typeof import('pinia')['mapStores']
|
||||||
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||||
|
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
15
src/main.ts
Normal file
15
src/main.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import 'vant/lib/index.css'
|
||||||
|
import './styles/global.scss'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
|
||||||
33
src/router/index.ts
Normal file
33
src/router/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/Home.vue'),
|
||||||
|
meta: { title: '文本阅读器' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reader/:id',
|
||||||
|
name: 'Reader',
|
||||||
|
component: () => import('@/views/Reader.vue'),
|
||||||
|
meta: { title: '阅读' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
if (to.meta.title) {
|
||||||
|
document.title = to.meta.title as string
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
||||||
|
|
||||||
79
src/styles/global.scss
Normal file
79
src/styles/global.scss
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表项动画 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文件卡片样式 */
|
||||||
|
.file-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阅读器样式 */
|
||||||
|
.reader-content {
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
text-indent: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
223
src/views/Home.vue
Normal file
223
src/views/Home.vue
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<van-nav-bar title="文本阅读器" fixed placeholder>
|
||||||
|
<template #right>
|
||||||
|
<van-icon name="search" size="18" @click="showSearch = true" />
|
||||||
|
</template>
|
||||||
|
</van-nav-bar>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<van-search
|
||||||
|
v-show="showSearch"
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索文件名或描述"
|
||||||
|
@cancel="showSearch = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 文件列表 -->
|
||||||
|
<div class="file-list">
|
||||||
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||||
|
<van-empty
|
||||||
|
v-if="filteredFiles.length === 0 && !loading"
|
||||||
|
description="暂无文件"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else class="file-items">
|
||||||
|
<div
|
||||||
|
v-for="file in filteredFiles"
|
||||||
|
:key="file.fileId"
|
||||||
|
class="file-card"
|
||||||
|
@click="openFile(file.fileId)"
|
||||||
|
>
|
||||||
|
<div class="file-header">
|
||||||
|
<van-icon name="notes-o" size="24" color="#1989fa" />
|
||||||
|
<span class="file-name">{{ file.originalFileName }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="file.description" class="file-description">
|
||||||
|
{{ file.description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-footer">
|
||||||
|
<span class="file-size">{{ formatFileSize(file.fileSize) }}</span>
|
||||||
|
<span class="file-date">{{ formatDate(file.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-pull-refresh>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<van-loading v-if="loading" class="loading" size="24px" vertical>
|
||||||
|
加载中...
|
||||||
|
</van-loading>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { getAllTextFiles, type TextFile } from '@/api/reader'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const files = ref<TextFile[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const showSearch = ref(false)
|
||||||
|
const searchText = ref('')
|
||||||
|
|
||||||
|
// 过滤后的文件列表
|
||||||
|
const filteredFiles = computed(() => {
|
||||||
|
if (!searchText.value) {
|
||||||
|
return files.value
|
||||||
|
}
|
||||||
|
const keyword = searchText.value.toLowerCase()
|
||||||
|
return files.value.filter(
|
||||||
|
(file) =>
|
||||||
|
file.originalFileName.toLowerCase().includes(keyword) ||
|
||||||
|
(file.description && file.description.toLowerCase().includes(keyword))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取文件列表
|
||||||
|
const fetchFiles = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await getAllTextFiles()
|
||||||
|
files.value = response.data || []
|
||||||
|
} catch (error) {
|
||||||
|
showToast('获取文件列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
const onRefresh = () => {
|
||||||
|
fetchFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
const openFile = (fileId: number) => {
|
||||||
|
router.push(`/reader/${fileId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (size: number): string => {
|
||||||
|
if (size < 1024) {
|
||||||
|
return `${size} B`
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return `${(size / 1024).toFixed(2)} KB`
|
||||||
|
} else {
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr: string): string => {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now.getTime() - date.getTime()
|
||||||
|
const day = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
if (diff < day) {
|
||||||
|
return '今天'
|
||||||
|
} else if (diff < 2 * day) {
|
||||||
|
return '昨天'
|
||||||
|
} else if (diff < 7 * day) {
|
||||||
|
return `${Math.floor(diff / day)}天前`
|
||||||
|
} else {
|
||||||
|
return `${date.getMonth() + 1}-${date.getDate()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchFiles()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.home-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-items {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #969799;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
|
||||||
|
.file-size,
|
||||||
|
.file-date {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
320
src/views/Reader.vue
Normal file
320
src/views/Reader.vue
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
<template>
|
||||||
|
<div class="reader-container">
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<van-nav-bar
|
||||||
|
:title="fileInfo?.fileName || '阅读'"
|
||||||
|
left-arrow
|
||||||
|
fixed
|
||||||
|
placeholder
|
||||||
|
@click-left="onBack"
|
||||||
|
>
|
||||||
|
<template #right>
|
||||||
|
<van-icon name="setting-o" size="18" @click="showSettings = true" />
|
||||||
|
</template>
|
||||||
|
</van-nav-bar>
|
||||||
|
|
||||||
|
<!-- 阅读内容 -->
|
||||||
|
<div class="reader-wrapper" :style="readerStyle">
|
||||||
|
<van-loading v-if="loading" class="loading" size="24px" vertical>
|
||||||
|
加载中...
|
||||||
|
</van-loading>
|
||||||
|
|
||||||
|
<div v-else class="reader-content" :style="contentStyle">
|
||||||
|
<h2 class="file-title">{{ fileInfo?.fileName }}</h2>
|
||||||
|
<div class="content-text" v-html="formattedContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置面板 -->
|
||||||
|
<van-popup
|
||||||
|
v-model:show="showSettings"
|
||||||
|
position="bottom"
|
||||||
|
:style="{ height: '40%' }"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
<div class="settings-panel">
|
||||||
|
<h3>阅读设置</h3>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>字体大小</span>
|
||||||
|
<div class="font-size-control">
|
||||||
|
<van-button
|
||||||
|
size="small"
|
||||||
|
@click="changeFontSize(-2)"
|
||||||
|
:disabled="fontSize <= 12"
|
||||||
|
>
|
||||||
|
A-
|
||||||
|
</van-button>
|
||||||
|
<span class="font-size-value">{{ fontSize }}px</span>
|
||||||
|
<van-button
|
||||||
|
size="small"
|
||||||
|
@click="changeFontSize(2)"
|
||||||
|
:disabled="fontSize >= 24"
|
||||||
|
>
|
||||||
|
A+
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>行间距</span>
|
||||||
|
<van-slider
|
||||||
|
v-model="lineHeight"
|
||||||
|
:min="1.2"
|
||||||
|
:max="2.5"
|
||||||
|
:step="0.1"
|
||||||
|
active-color="#1989fa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>背景颜色</span>
|
||||||
|
<div class="theme-colors">
|
||||||
|
<div
|
||||||
|
v-for="theme in themes"
|
||||||
|
:key="theme.name"
|
||||||
|
class="theme-item"
|
||||||
|
:class="{ active: currentTheme === theme.name }"
|
||||||
|
:style="{ backgroundColor: theme.bgColor }"
|
||||||
|
@click="changeTheme(theme.name)"
|
||||||
|
>
|
||||||
|
<van-icon
|
||||||
|
v-if="currentTheme === theme.name"
|
||||||
|
name="success"
|
||||||
|
color="#fff"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { readFileContent, type TextFileContent } from '@/api/reader'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const fileInfo = ref<TextFileContent | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const showSettings = ref(false)
|
||||||
|
|
||||||
|
// 阅读设置
|
||||||
|
const fontSize = ref(16)
|
||||||
|
const lineHeight = ref(1.8)
|
||||||
|
const currentTheme = ref('white')
|
||||||
|
|
||||||
|
const themes = [
|
||||||
|
{ name: 'white', bgColor: '#ffffff', textColor: '#333333' },
|
||||||
|
{ name: 'yellow', bgColor: '#fef8e6', textColor: '#333333' },
|
||||||
|
{ name: 'green', bgColor: '#e3f5e1', textColor: '#333333' },
|
||||||
|
{ name: 'gray', bgColor: '#f0f0f0', textColor: '#333333' },
|
||||||
|
{ name: 'dark', bgColor: '#1f1f1f', textColor: '#cccccc' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 计算样式
|
||||||
|
const readerStyle = computed(() => {
|
||||||
|
const theme = themes.find((t) => t.name === currentTheme.value)
|
||||||
|
return {
|
||||||
|
backgroundColor: theme?.bgColor || '#ffffff'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const contentStyle = computed(() => {
|
||||||
|
const theme = themes.find((t) => t.name === currentTheme.value)
|
||||||
|
return {
|
||||||
|
fontSize: `${fontSize.value}px`,
|
||||||
|
lineHeight: lineHeight.value,
|
||||||
|
color: theme?.textColor || '#333333'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化内容(将换行符转换为段落)
|
||||||
|
const formattedContent = computed(() => {
|
||||||
|
if (!fileInfo.value?.content) return ''
|
||||||
|
|
||||||
|
return fileInfo.value.content
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim())
|
||||||
|
.map((line) => `<p>${line}</p>`)
|
||||||
|
.join('')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取文件内容
|
||||||
|
const fetchContent = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const fileId = Number(route.params.id)
|
||||||
|
const response = await readFileContent(fileId)
|
||||||
|
fileInfo.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
showToast('获取文件内容失败')
|
||||||
|
router.back()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const onBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改字体大小
|
||||||
|
const changeFontSize = (delta: number) => {
|
||||||
|
const newSize = fontSize.value + delta
|
||||||
|
if (newSize >= 12 && newSize <= 24) {
|
||||||
|
fontSize.value = newSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换主题
|
||||||
|
const changeTheme = (themeName: string) => {
|
||||||
|
currentTheme.value = themeName
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchContent()
|
||||||
|
|
||||||
|
// 从localStorage读取设置
|
||||||
|
const savedSettings = localStorage.getItem('readerSettings')
|
||||||
|
if (savedSettings) {
|
||||||
|
try {
|
||||||
|
const settings = JSON.parse(savedSettings)
|
||||||
|
fontSize.value = settings.fontSize || 16
|
||||||
|
lineHeight.value = settings.lineHeight || 1.8
|
||||||
|
currentTheme.value = settings.theme || 'white'
|
||||||
|
} catch (e) {
|
||||||
|
console.error('读取设置失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
const saveSettings = () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'readerSettings',
|
||||||
|
JSON.stringify({
|
||||||
|
fontSize: fontSize.value,
|
||||||
|
lineHeight: lineHeight.value,
|
||||||
|
theme: currentTheme.value
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听设置变化并保存
|
||||||
|
watch([fontSize, lineHeight, currentTheme], () => {
|
||||||
|
saveSettings()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.reader-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
.file-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
:deep(p) {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
text-indent: 2em;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #646566;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.font-size-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #323233;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-colors {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.theme-item {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #1989fa;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
33
tsconfig.json
Normal file
33
tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path alias */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
12
tsconfig.node.json
Normal file
12
tsconfig.node.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
37
vite.config.ts
Normal file
37
vite.config.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { VantResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
Components({
|
||||||
|
resolvers: [VantResolver()],
|
||||||
|
}),
|
||||||
|
AutoImport({
|
||||||
|
imports: ['vue', 'vue-router', 'pinia'],
|
||||||
|
dts: 'src/auto-imports.d.ts',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:18080', // 统一通过Gateway访问
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user