feat: 完成阶段四前端开发和阶段五部署准备
阶段四:前端开发 - 管理后台 (worklog-web): Vue 3 + Element Plus - 登录页面、主布局、人员管理、模板管理、工作日志 - baseURL: /wladmin/api/v1 - 移动端 H5 (worklog-mobile): Vue 3 + Vant 4 - 登录、首页、日志列表、新建/编辑/详情页 - baseURL: /wlmobile/api/v1 阶段五:部署准备 - 后端打包: worklog-api-1.0.0.jar (48MB) - 前端打包: worklog-web (1.6MB), worklog-mobile (632KB) - 单元测试: 29个测试全部通过 - API端口调整为 8200 - Nginx配置更新 配置变更 - 后端端口: 8080 → 8200 - 前端 baseURL: /wlog → /wladmin, /wlmobile - Nginx 代理路径更新
This commit is contained in:
parent
dbcc06edbc
commit
4b4fcf2ead
2
.gitignore
vendored
2
.gitignore
vendored
@ -31,7 +31,7 @@ dist/
|
|||||||
# ==================== 日志文件 ====================
|
# ==================== 日志文件 ====================
|
||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
log/
|
/log/
|
||||||
|
|
||||||
# ==================== 临时文件 ====================
|
# ==================== 临时文件 ====================
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|||||||
@ -24,9 +24,9 @@ server {
|
|||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
|
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
|
||||||
|
|
||||||
# 管理后台
|
# 管理后台前端静态资源
|
||||||
location /admin/ {
|
location /admin/ {
|
||||||
alias /opt/worklog/worklog-admin/dist/;
|
alias /opt/worklog/worklog-web/dist/;
|
||||||
try_files $uri $uri/ /admin/index.html;
|
try_files $uri $uri/ /admin/index.html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 移动端 H5
|
# 移动端 H5 前端静态资源
|
||||||
location /mobile/ {
|
location /mobile/ {
|
||||||
alias /opt/worklog/worklog-mobile/dist/;
|
alias /opt/worklog/worklog-mobile/dist/;
|
||||||
try_files $uri $uri/ /mobile/index.html;
|
try_files $uri $uri/ /mobile/index.html;
|
||||||
@ -50,9 +50,9 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 后端 API 代理
|
# 管理后台 API 代理 (/wladmin/api/v1 → /wlog/api/v1)
|
||||||
location /api/ {
|
location /wladmin/api/ {
|
||||||
proxy_pass http://127.0.0.1:8080;
|
proxy_pass http://127.0.0.1:8200/wlog/api/;
|
||||||
|
|
||||||
# 代理头设置
|
# 代理头设置
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@ -70,16 +70,33 @@ server {
|
|||||||
proxy_buffer_size 4k;
|
proxy_buffer_size 4k;
|
||||||
proxy_buffers 8 4k;
|
proxy_buffers 8 4k;
|
||||||
proxy_busy_buffers_size 8k;
|
proxy_busy_buffers_size 8k;
|
||||||
|
}
|
||||||
|
|
||||||
# WebSocket 支持(如需要)
|
# 移动端 API 代理 (/wlmobile/api/v1 → /wlog/api/v1)
|
||||||
# proxy_http_version 1.1;
|
location /wlmobile/api/ {
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
proxy_pass http://127.0.0.1:8200/wlog/api/;
|
||||||
# proxy_set_header Connection "upgrade";
|
|
||||||
|
# 代理头设置
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# 超时设置
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
|
# 缓冲设置
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 4k;
|
||||||
|
proxy_buffers 8 4k;
|
||||||
|
proxy_busy_buffers_size 8k;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Swagger API 文档(生产环境建议关闭或限制访问)
|
# Swagger API 文档(生产环境建议关闭或限制访问)
|
||||||
location /swagger-ui.html {
|
location /wlog/swagger-ui.html {
|
||||||
proxy_pass http://127.0.0.1:8080;
|
proxy_pass http://127.0.0.1:8200;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
@ -90,8 +107,8 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 健康检查接口
|
# 健康检查接口
|
||||||
location /api/v1/health {
|
location /wlog/api/v1/health {
|
||||||
proxy_pass http://127.0.0.1:8080;
|
proxy_pass http://127.0.0.1:8200;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
access_log off; # 健康检查不记录日志
|
access_log off; # 健康检查不记录日志
|
||||||
}
|
}
|
||||||
|
|||||||
24
worklog-mobile/.gitignore
vendored
Normal file
24
worklog-mobile/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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?
|
||||||
13
worklog-mobile/index.html
Normal file
13
worklog-mobile/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!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, minimum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>工作日志</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
worklog-mobile/package.json
Normal file
29
worklog-mobile/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "worklog-mobile",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"vant": "^4.8.4",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-router": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.24",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"autoprefixer": "^10.4.18",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"postcss-px-to-viewport": "^1.1.1",
|
||||||
|
"typescript": "~5.4.2",
|
||||||
|
"vite": "^5.1.5",
|
||||||
|
"vue-tsc": "^2.0.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
worklog-mobile/public/vite.svg
Normal file
1
worklog-mobile/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFBD4F"></stop><stop offset="100%" stop-color="#FF980E"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
12
worklog-mobile/src/App.vue
Normal file
12
worklog-mobile/src/App.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
worklog-mobile/src/api/auth.ts
Normal file
32
worklog-mobile/src/api/auth.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 认证相关 API
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
// 登录请求参数
|
||||||
|
export interface LoginParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录响应
|
||||||
|
export interface LoginResult {
|
||||||
|
token: string
|
||||||
|
userInfo: UserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
export interface UserInfo {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
name: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
export function login(data: LoginParams): Promise<LoginResult> {
|
||||||
|
return request.post('/auth/login', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
export function logout(): Promise<void> {
|
||||||
|
return request.post('/auth/logout')
|
||||||
|
}
|
||||||
69
worklog-mobile/src/api/log.ts
Normal file
69
worklog-mobile/src/api/log.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// 日志相关 API
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
// 日志信息
|
||||||
|
export interface Log {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
logDate: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
templateId: string
|
||||||
|
createdTime: string
|
||||||
|
updatedTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页请求参数
|
||||||
|
export interface PageParams {
|
||||||
|
pageNum: number
|
||||||
|
pageSize: number
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页结果
|
||||||
|
export interface PageResult<T> {
|
||||||
|
pageNum: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
list: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志参数
|
||||||
|
export interface CreateLogParams {
|
||||||
|
logDate: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
templateId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志参数
|
||||||
|
export interface UpdateLogParams {
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询我的日志
|
||||||
|
export function pageMyLogs(params: PageParams): Promise<PageResult<Log>> {
|
||||||
|
return request.get('/log/page', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志详情
|
||||||
|
export function getLogById(id: string): Promise<Log> {
|
||||||
|
return request.get(`/log/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志
|
||||||
|
export function createLog(data: CreateLogParams): Promise<Log> {
|
||||||
|
return request.post('/log', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志
|
||||||
|
export function updateLog(id: string, data: UpdateLogParams): Promise<Log> {
|
||||||
|
return request.put(`/log/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除日志
|
||||||
|
export function deleteLog(id: string): Promise<void> {
|
||||||
|
return request.delete(`/log/${id}`)
|
||||||
|
}
|
||||||
16
worklog-mobile/src/api/template.ts
Normal file
16
worklog-mobile/src/api/template.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// 模板相关 API
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
// 模板信息
|
||||||
|
export interface Template {
|
||||||
|
id: string
|
||||||
|
templateName: string
|
||||||
|
content: string
|
||||||
|
status: number
|
||||||
|
createdTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取启用的模板列表
|
||||||
|
export function listEnabledTemplates(): Promise<Template[]> {
|
||||||
|
return request.get('/template/enabled')
|
||||||
|
}
|
||||||
23
worklog-mobile/src/assets/main.css
Normal file
23
worklog-mobile/src/assets/main.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-nav-bar {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding-top: 46px;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
15
worklog-mobile/src/main.ts
Normal file
15
worklog-mobile/src/main.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import Vant from 'vant'
|
||||||
|
import 'vant/lib/index.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(Vant)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
64
worklog-mobile/src/router/index.ts
Normal file
64
worklog-mobile/src/router/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { TOKEN_KEY } from '@/utils/constants'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('@/views/login/index.vue'),
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/log',
|
||||||
|
name: 'LogList',
|
||||||
|
component: () => import('@/views/log/index.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/log/create',
|
||||||
|
name: 'LogCreate',
|
||||||
|
component: () => import('@/views/log/create.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/log/edit/:id',
|
||||||
|
name: 'LogEdit',
|
||||||
|
component: () => import('@/views/log/edit.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/log/detail/:id',
|
||||||
|
name: 'LogDetail',
|
||||||
|
component: () => import('@/views/log/detail.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
|
|
||||||
|
if (to.meta.requiresAuth === false) {
|
||||||
|
next()
|
||||||
|
} else if (!token && to.path !== '/login') {
|
||||||
|
next('/login')
|
||||||
|
} else if (token && to.path === '/login') {
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
43
worklog-mobile/src/store/user.ts
Normal file
43
worklog-mobile/src/store/user.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { login as loginApi, logout as logoutApi } from '@/api/auth'
|
||||||
|
import type { LoginParams, UserInfo } from '@/api/auth'
|
||||||
|
import { TOKEN_KEY, USER_INFO_KEY } from '@/utils/constants'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY))
|
||||||
|
const userInfo = ref<UserInfo | null>(
|
||||||
|
JSON.parse(localStorage.getItem(USER_INFO_KEY) || 'null')
|
||||||
|
)
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
async function login(params: LoginParams) {
|
||||||
|
const result = await loginApi(params)
|
||||||
|
token.value = result.token
|
||||||
|
userInfo.value = result.userInfo
|
||||||
|
|
||||||
|
localStorage.setItem(TOKEN_KEY, result.token)
|
||||||
|
localStorage.setItem(USER_INFO_KEY, JSON.stringify(result.userInfo))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await logoutApi()
|
||||||
|
} finally {
|
||||||
|
token.value = null
|
||||||
|
userInfo.value = null
|
||||||
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
|
localStorage.removeItem(USER_INFO_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
userInfo,
|
||||||
|
login,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
})
|
||||||
3
worklog-mobile/src/utils/constants.ts
Normal file
3
worklog-mobile/src/utils/constants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// 常量定义
|
||||||
|
export const TOKEN_KEY = 'worklog_token'
|
||||||
|
export const USER_INFO_KEY = 'worklog_user_info'
|
||||||
73
worklog-mobile/src/utils/request.ts
Normal file
73
worklog-mobile/src/utils/request.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import { showFailToast } from 'vant'
|
||||||
|
import { TOKEN_KEY } from '@/utils/constants'
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const service: AxiosInstance = axios.create({
|
||||||
|
baseURL: '/wlmobile/api/v1',
|
||||||
|
timeout: 15000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
const res = response.data
|
||||||
|
|
||||||
|
// 业务成功
|
||||||
|
if (res.success) {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务失败
|
||||||
|
showFailToast(res.message || '请求失败')
|
||||||
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 401 未登录或 Token 过期
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
|
localStorage.removeItem('worklog_user_info')
|
||||||
|
window.location.href = '/login'
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
showFailToast(error.response?.data?.message || '网络错误')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 封装请求方法
|
||||||
|
export const request = {
|
||||||
|
get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.get(url, config)
|
||||||
|
},
|
||||||
|
|
||||||
|
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.post(url, data, config)
|
||||||
|
},
|
||||||
|
|
||||||
|
put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.put(url, data, config)
|
||||||
|
},
|
||||||
|
|
||||||
|
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.delete(url, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default service
|
||||||
80
worklog-mobile/src/views/home/index.vue
Normal file
80
worklog-mobile/src/views/home/index.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-page">
|
||||||
|
<van-nav-bar title="工作日志" />
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
<!-- 用户信息 -->
|
||||||
|
<van-cell-group inset class="user-card">
|
||||||
|
<van-cell :title="userStore.userInfo?.name" :label="userStore.userInfo?.username">
|
||||||
|
<template #icon>
|
||||||
|
<van-icon name="user-o" size="24" style="margin-right: 8px" />
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 快捷入口 -->
|
||||||
|
<van-cell-group inset title="快捷入口" class="menu-group">
|
||||||
|
<van-cell title="我的日志" is-link to="/log">
|
||||||
|
<template #icon>
|
||||||
|
<van-icon name="notes-o" size="20" style="margin-right: 8px" />
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
<van-cell title="新建日志" is-link to="/log/create">
|
||||||
|
<template #icon>
|
||||||
|
<van-icon name="edit" size="20" style="margin-right: 8px" />
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 退出登录 -->
|
||||||
|
<div class="logout-btn">
|
||||||
|
<van-button round block type="danger" @click="handleLogout">退出登录</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部导航 -->
|
||||||
|
<van-tabbar v-model="active" route>
|
||||||
|
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
|
||||||
|
<van-tabbar-item icon="notes-o" to="/log">日志</van-tabbar-item>
|
||||||
|
</van-tabbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const active = ref(0)
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
await showConfirmDialog({
|
||||||
|
title: '提示',
|
||||||
|
message: '确定要退出登录吗?'
|
||||||
|
})
|
||||||
|
await userStore.logout()
|
||||||
|
showSuccessToast('已退出登录')
|
||||||
|
router.push('/login')
|
||||||
|
} catch {
|
||||||
|
// 取消操作
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-card {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-group {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
margin: 32px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
162
worklog-mobile/src/views/log/create.vue
Normal file
162
worklog-mobile/src/views/log/create.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-create-page">
|
||||||
|
<van-nav-bar title="新建日志" left-arrow @click-left="goBack" />
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
<van-form @submit="handleSubmit">
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-field
|
||||||
|
v-model="form.logDate"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="logDate"
|
||||||
|
label="日期"
|
||||||
|
placeholder="选择日期"
|
||||||
|
:rules="[{ required: true, message: '请选择日期' }]"
|
||||||
|
@click="showDatePicker = true"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.title"
|
||||||
|
name="title"
|
||||||
|
label="标题"
|
||||||
|
placeholder="请输入标题"
|
||||||
|
:rules="[{ required: true, message: '请输入标题' }]"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.templateId"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="templateId"
|
||||||
|
label="模板"
|
||||||
|
placeholder="选择模板(可选)"
|
||||||
|
@click="showTemplatePicker = true"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.content"
|
||||||
|
name="content"
|
||||||
|
label="内容"
|
||||||
|
type="textarea"
|
||||||
|
rows="8"
|
||||||
|
autosize
|
||||||
|
placeholder="请输入日志内容(支持Markdown)"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<div class="submit-btn">
|
||||||
|
<van-button round block type="primary" native-type="submit" :loading="loading">
|
||||||
|
提交
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</van-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<van-popup v-model:show="showDatePicker" position="bottom" round>
|
||||||
|
<van-date-picker
|
||||||
|
v-model="selectedDate"
|
||||||
|
title="选择日期"
|
||||||
|
@confirm="onDateConfirm"
|
||||||
|
@cancel="showDatePicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<!-- 模板选择器 -->
|
||||||
|
<van-popup v-model:show="showTemplatePicker" position="bottom" round>
|
||||||
|
<van-picker
|
||||||
|
:columns="templateColumns"
|
||||||
|
title="选择模板"
|
||||||
|
@confirm="onTemplateConfirm"
|
||||||
|
@cancel="showTemplatePicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showSuccessToast } from 'vant'
|
||||||
|
import { createLog } from '@/api/log'
|
||||||
|
import { listEnabledTemplates } from '@/api/template'
|
||||||
|
import type { Template } from '@/api/template'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const showDatePicker = ref(false)
|
||||||
|
const showTemplatePicker = ref(false)
|
||||||
|
const selectedDate = ref(['2024', '01', '01'])
|
||||||
|
const templates = ref<Template[]>([])
|
||||||
|
const templateColumns = ref<{ text: string; value: string }[]>([])
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
logDate: '',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
templateId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 设置默认日期为今天
|
||||||
|
const today = new Date()
|
||||||
|
const year = today.getFullYear().toString()
|
||||||
|
const month = (today.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const day = today.getDate().toString().padStart(2, '0')
|
||||||
|
selectedDate.value = [year, month, day]
|
||||||
|
form.logDate = `${year}-${month}-${day}`
|
||||||
|
|
||||||
|
// 加载模板列表
|
||||||
|
try {
|
||||||
|
templates.value = await listEnabledTemplates()
|
||||||
|
templateColumns.value = templates.value.map(t => ({
|
||||||
|
text: t.templateName,
|
||||||
|
value: t.id
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onDateConfirm() {
|
||||||
|
form.logDate = selectedDate.value.join('-')
|
||||||
|
showDatePicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTemplateConfirm({ selectedValues }: { selectedValues: string[] }) {
|
||||||
|
const templateId = selectedValues[0]
|
||||||
|
form.templateId = templateId
|
||||||
|
|
||||||
|
// 填充模板内容
|
||||||
|
const template = templates.value.find(t => t.id === templateId)
|
||||||
|
if (template && template.content) {
|
||||||
|
form.content = template.content
|
||||||
|
}
|
||||||
|
|
||||||
|
showTemplatePicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await createLog({
|
||||||
|
logDate: form.logDate,
|
||||||
|
title: form.title,
|
||||||
|
content: form.content,
|
||||||
|
templateId: form.templateId || undefined
|
||||||
|
})
|
||||||
|
showSuccessToast('创建成功')
|
||||||
|
router.push('/log')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.submit-btn {
|
||||||
|
margin: 24px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
worklog-mobile/src/views/log/detail.vue
Normal file
97
worklog-mobile/src/views/log/detail.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-detail-page">
|
||||||
|
<van-nav-bar title="日志详情" left-arrow @click-left="goBack">
|
||||||
|
<template #right>
|
||||||
|
<van-icon name="edit" size="18" @click="goEdit" />
|
||||||
|
</template>
|
||||||
|
</van-nav-bar>
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-cell title="日期" :value="log.logDate" />
|
||||||
|
<van-cell title="标题" :value="log.title" />
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<van-cell-group inset title="内容">
|
||||||
|
<div class="content-box">
|
||||||
|
<pre>{{ log.content || '暂无内容' }}</pre>
|
||||||
|
</div>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<div class="action-btn">
|
||||||
|
<van-button round block type="danger" @click="handleDelete">删除日志</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||||
|
import { getLogById, deleteLog } from '@/api/log'
|
||||||
|
import type { Log } from '@/api/log'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const log = ref<Log>({
|
||||||
|
id: '',
|
||||||
|
userId: '',
|
||||||
|
logDate: '',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
templateId: '',
|
||||||
|
createdTime: '',
|
||||||
|
updatedTime: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const id = route.params.id as string
|
||||||
|
try {
|
||||||
|
log.value = await getLogById(id)
|
||||||
|
} catch {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goEdit() {
|
||||||
|
router.push(`/log/edit/${log.value.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
try {
|
||||||
|
await showConfirmDialog({
|
||||||
|
title: '提示',
|
||||||
|
message: '确定要删除该日志吗?'
|
||||||
|
})
|
||||||
|
await deleteLog(log.value.id)
|
||||||
|
showSuccessToast('删除成功')
|
||||||
|
router.push('/log')
|
||||||
|
} catch {
|
||||||
|
// 取消操作
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-box {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-box pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
margin: 24px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
96
worklog-mobile/src/views/log/edit.vue
Normal file
96
worklog-mobile/src/views/log/edit.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-edit-page">
|
||||||
|
<van-nav-bar title="编辑日志" left-arrow @click-left="goBack" />
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
<van-form @submit="handleSubmit">
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-field
|
||||||
|
v-model="form.logDate"
|
||||||
|
name="logDate"
|
||||||
|
label="日期"
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.title"
|
||||||
|
name="title"
|
||||||
|
label="标题"
|
||||||
|
placeholder="请输入标题"
|
||||||
|
:rules="[{ required: true, message: '请输入标题' }]"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.content"
|
||||||
|
name="content"
|
||||||
|
label="内容"
|
||||||
|
type="textarea"
|
||||||
|
rows="8"
|
||||||
|
autosize
|
||||||
|
placeholder="请输入日志内容"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<div class="submit-btn">
|
||||||
|
<van-button round block type="primary" native-type="submit" :loading="loading">
|
||||||
|
保存
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</van-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { showSuccessToast, showFailToast } from 'vant'
|
||||||
|
import { getLogById, updateLog } from '@/api/log'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
logDate: '',
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const id = route.params.id as string
|
||||||
|
try {
|
||||||
|
const log = await getLogById(id)
|
||||||
|
form.logDate = log.logDate
|
||||||
|
form.title = log.title
|
||||||
|
form.content = log.content || ''
|
||||||
|
} catch {
|
||||||
|
showFailToast('加载失败')
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const id = route.params.id as string
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await updateLog(id, {
|
||||||
|
title: form.title,
|
||||||
|
content: form.content
|
||||||
|
})
|
||||||
|
showSuccessToast('保存成功')
|
||||||
|
router.push('/log')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.submit-btn {
|
||||||
|
margin: 24px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
146
worklog-mobile/src/views/log/index.vue
Normal file
146
worklog-mobile/src/views/log/index.vue
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-list-page">
|
||||||
|
<van-nav-bar title="我的日志" />
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<van-cell-group inset class="search-group">
|
||||||
|
<van-field
|
||||||
|
v-model="dateRange"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
label="日期范围"
|
||||||
|
placeholder="选择日期范围"
|
||||||
|
@click="showDatePicker = true"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 日志列表 -->
|
||||||
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||||
|
<van-list
|
||||||
|
v-model:loading="loading"
|
||||||
|
:finished="finished"
|
||||||
|
finished-text="没有更多了"
|
||||||
|
@load="onLoad"
|
||||||
|
>
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-cell
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.id"
|
||||||
|
:title="item.title"
|
||||||
|
:label="item.logDate"
|
||||||
|
is-link
|
||||||
|
@click="goDetail(item.id)"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
<span class="log-date">{{ item.logDate }}</span>
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
</van-list>
|
||||||
|
</van-pull-refresh>
|
||||||
|
|
||||||
|
<!-- 新建按钮 -->
|
||||||
|
<van-floating-bubble icon="plus" @click="goCreate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<van-popup v-model:show="showDatePicker" position="bottom" round>
|
||||||
|
<van-date-picker
|
||||||
|
v-model="selectedDate"
|
||||||
|
type="daterange"
|
||||||
|
title="选择日期范围"
|
||||||
|
@confirm="onDateConfirm"
|
||||||
|
@cancel="showDatePicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<!-- 底部导航 -->
|
||||||
|
<van-tabbar v-model="active" route>
|
||||||
|
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
|
||||||
|
<van-tabbar-item icon="notes-o" to="/log">日志</van-tabbar-item>
|
||||||
|
</van-tabbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { pageMyLogs } from '@/api/log'
|
||||||
|
import type { Log } from '@/api/log'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const active = ref(1)
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const showDatePicker = ref(false)
|
||||||
|
const dateRange = ref('')
|
||||||
|
const selectedDate = ref<string[]>([])
|
||||||
|
|
||||||
|
const list = ref<Log[]>([])
|
||||||
|
const pageNum = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const result = await pageMyLogs({
|
||||||
|
pageNum: pageNum.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
})
|
||||||
|
list.value = result.list
|
||||||
|
total.value = result.total
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onLoad() {
|
||||||
|
if (list.value.length >= total.value) {
|
||||||
|
finished.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pageNum.value++
|
||||||
|
const result = await pageMyLogs({
|
||||||
|
pageNum: pageNum.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
})
|
||||||
|
list.value.push(...result.list)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRefresh() {
|
||||||
|
pageNum.value = 1
|
||||||
|
await loadData()
|
||||||
|
refreshing.value = false
|
||||||
|
finished.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDateConfirm() {
|
||||||
|
showDatePicker.value = false
|
||||||
|
// 重新加载数据
|
||||||
|
pageNum.value = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(id: string) {
|
||||||
|
router.push(`/log/detail/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCreate() {
|
||||||
|
router.push('/log/create')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-group {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-date {
|
||||||
|
color: #969799;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
76
worklog-mobile/src/views/login/index.vue
Normal file
76
worklog-mobile/src/views/login/index.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<van-nav-bar title="工作日志" />
|
||||||
|
|
||||||
|
<div class="login-content">
|
||||||
|
<van-form @submit="handleLogin">
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-field
|
||||||
|
v-model="form.username"
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
:rules="[{ required: true, message: '请输入用户名' }]"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
label="密码"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:rules="[{ required: true, message: '请输入密码' }]"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<div class="login-btn">
|
||||||
|
<van-button round block type="primary" native-type="submit" :loading="loading">
|
||||||
|
登录
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</van-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showSuccessToast } from 'vant'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await userStore.login(form)
|
||||||
|
showSuccessToast('登录成功')
|
||||||
|
router.push('/')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
padding: 60px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
margin: 24px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
worklog-mobile/tsconfig.json
Normal file
31
worklog-mobile/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"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/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
11
worklog-mobile/tsconfig.node.json
Normal file
11
worklog-mobile/tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
1
worklog-mobile/tsconfig.node.tsbuildinfo
Normal file
1
worklog-mobile/tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
2
worklog-mobile/vite.config.d.ts
vendored
Normal file
2
worklog-mobile/vite.config.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
declare const _default: import("vite").UserConfig;
|
||||||
|
export default _default;
|
||||||
21
worklog-mobile/vite.config.js
Normal file
21
worklog-mobile/vite.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5174,
|
||||||
|
proxy: {
|
||||||
|
'/wlmobile': {
|
||||||
|
target: 'http://localhost:8200',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
22
worklog-mobile/vite.config.ts
Normal file
22
worklog-mobile/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5174,
|
||||||
|
proxy: {
|
||||||
|
'/wlmobile': {
|
||||||
|
target: 'http://localhost:8200',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
24
worklog-web/.gitignore
vendored
Normal file
24
worklog-web/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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?
|
||||||
5
worklog-web/README.md
Normal file
5
worklog-web/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||||
13
worklog-web/index.html
Normal file
13
worklog-web/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<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" />
|
||||||
|
<title>worklog-web</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
worklog-web/package.json
Normal file
27
worklog-web/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "worklog-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"axios": "^1.13.5",
|
||||||
|
"element-plus": "^2.13.2",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.25",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
"vue-tsc": "^3.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
worklog-web/public/vite.svg
Normal file
1
worklog-web/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
14
worklog-web/src/App.vue
Normal file
14
worklog-web/src/App.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body, #app {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
worklog-web/src/api/auth.ts
Normal file
32
worklog-web/src/api/auth.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 认证相关 API
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
// 登录请求参数
|
||||||
|
export interface LoginParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录响应
|
||||||
|
export interface LoginResult {
|
||||||
|
token: string
|
||||||
|
userInfo: UserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
export interface UserInfo {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
name: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
export function login(data: LoginParams): Promise<LoginResult> {
|
||||||
|
return request.post('/auth/login', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
export function logout(): Promise<void> {
|
||||||
|
return request.post('/auth/logout')
|
||||||
|
}
|
||||||
70
worklog-web/src/api/log.ts
Normal file
70
worklog-web/src/api/log.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// 日志管理相关 API
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
import type { PageResult } from './user'
|
||||||
|
|
||||||
|
// 日志信息
|
||||||
|
export interface Log {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
logDate: string
|
||||||
|
title: string
|
||||||
|
content?: string
|
||||||
|
templateId?: string
|
||||||
|
createdTime?: string
|
||||||
|
updatedTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志参数
|
||||||
|
export interface CreateLogParams {
|
||||||
|
logDate?: string
|
||||||
|
title: string
|
||||||
|
content?: string
|
||||||
|
templateId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志参数
|
||||||
|
export interface UpdateLogParams {
|
||||||
|
title?: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询我的日志
|
||||||
|
export function pageMyLogs(params: {
|
||||||
|
pageNum?: number
|
||||||
|
pageSize?: number
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}): Promise<PageResult<Log>> {
|
||||||
|
return request.get('/log/page', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询所有日志(管理员)
|
||||||
|
export function pageAllLogs(params: {
|
||||||
|
pageNum?: number
|
||||||
|
pageSize?: number
|
||||||
|
userId?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}): Promise<PageResult<Log>> {
|
||||||
|
return request.get('/log/page/all', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志详情
|
||||||
|
export function getLogById(id: string): Promise<Log> {
|
||||||
|
return request.get(`/log/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志
|
||||||
|
export function createLog(data: CreateLogParams): Promise<Log> {
|
||||||
|
return request.post('/log', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志
|
||||||
|
export function updateLog(id: string, data: UpdateLogParams): Promise<Log> {
|
||||||
|
return request.put(`/log/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除日志
|
||||||
|
export function deleteLog(id: string): Promise<void> {
|
||||||
|
return request.delete(`/log/${id}`)
|
||||||
|
}
|
||||||
59
worklog-web/src/api/template.ts
Normal file
59
worklog-web/src/api/template.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// 模板管理相关 API
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
// 模板信息
|
||||||
|
export interface Template {
|
||||||
|
id: string
|
||||||
|
templateName: string
|
||||||
|
content?: string
|
||||||
|
status: number
|
||||||
|
createdTime?: string
|
||||||
|
updatedTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建模板参数
|
||||||
|
export interface CreateTemplateParams {
|
||||||
|
templateName: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新模板参数
|
||||||
|
export interface UpdateTemplateParams {
|
||||||
|
templateName?: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取启用的模板列表
|
||||||
|
export function listEnabledTemplates(): Promise<Template[]> {
|
||||||
|
return request.get('/template/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有模板列表
|
||||||
|
export function listAllTemplates(): Promise<Template[]> {
|
||||||
|
return request.get('/template/list/all')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模板详情
|
||||||
|
export function getTemplateById(id: string): Promise<Template> {
|
||||||
|
return request.get(`/template/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建模板
|
||||||
|
export function createTemplate(data: CreateTemplateParams): Promise<Template> {
|
||||||
|
return request.post('/template', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新模板
|
||||||
|
export function updateTemplate(id: string, data: UpdateTemplateParams): Promise<Template> {
|
||||||
|
return request.put(`/template/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新模板状态
|
||||||
|
export function updateTemplateStatus(id: string, status: number): Promise<void> {
|
||||||
|
return request.put(`/template/${id}/status`, { status })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除模板
|
||||||
|
export function deleteTemplate(id: string): Promise<void> {
|
||||||
|
return request.delete(`/template/${id}`)
|
||||||
|
}
|
||||||
87
worklog-web/src/api/user.ts
Normal file
87
worklog-web/src/api/user.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// 用户管理相关 API
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
name: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
position?: string
|
||||||
|
description?: string
|
||||||
|
status: number
|
||||||
|
role: string
|
||||||
|
createdTime?: string
|
||||||
|
updatedTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页结果
|
||||||
|
export interface PageResult<T> {
|
||||||
|
pageNum: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
list: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户参数
|
||||||
|
export interface CreateUserParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
name: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
position?: string
|
||||||
|
description?: string
|
||||||
|
role?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户参数
|
||||||
|
export interface UpdateUserParams {
|
||||||
|
name?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
position?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询用户
|
||||||
|
export function pageUsers(params: {
|
||||||
|
pageNum?: number
|
||||||
|
pageSize?: number
|
||||||
|
name?: string
|
||||||
|
username?: string
|
||||||
|
status?: number
|
||||||
|
}): Promise<PageResult<User>> {
|
||||||
|
return request.get('/user/page', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户详情
|
||||||
|
export function getUserById(id: string): Promise<User> {
|
||||||
|
return request.get(`/user/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
export function createUser(data: CreateUserParams): Promise<User> {
|
||||||
|
return request.post('/user', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户
|
||||||
|
export function updateUser(id: string, data: UpdateUserParams): Promise<User> {
|
||||||
|
return request.put(`/user/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户状态
|
||||||
|
export function updateUserStatus(id: string, status: number): Promise<void> {
|
||||||
|
return request.put(`/user/${id}/status`, { status })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
export function deleteUser(id: string): Promise<void> {
|
||||||
|
return request.delete(`/user/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
export function resetPassword(id: string, newPassword: string): Promise<void> {
|
||||||
|
return request.put(`/user/${id}/password`, null, { params: { newPassword } })
|
||||||
|
}
|
||||||
1
worklog-web/src/assets/vue.svg
Normal file
1
worklog-web/src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
20
worklog-web/src/main.ts
Normal file
20
worklog-web/src/main.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import router from './router'
|
||||||
|
import pinia from './store'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './styles/index.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册所有图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.use(router)
|
||||||
|
app.use(pinia)
|
||||||
|
app.mount('#app')
|
||||||
60
worklog-web/src/router/index.ts
Normal file
60
worklog-web/src/router/index.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { TOKEN_KEY } from '@/utils/constants'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('@/views/login/index.vue'),
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Layout',
|
||||||
|
component: () => import('@/views/layout/index.vue'),
|
||||||
|
redirect: '/log',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'user',
|
||||||
|
name: 'User',
|
||||||
|
component: () => import('@/views/user/index.vue'),
|
||||||
|
meta: { title: '人员管理', icon: 'User' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'template',
|
||||||
|
name: 'Template',
|
||||||
|
component: () => import('@/views/template/index.vue'),
|
||||||
|
meta: { title: '模板管理', icon: 'Document' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'log',
|
||||||
|
name: 'Log',
|
||||||
|
component: () => import('@/views/log/index.vue'),
|
||||||
|
meta: { title: '工作日志', icon: 'Notebook' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
|
|
||||||
|
if (to.meta.requiresAuth === false) {
|
||||||
|
next()
|
||||||
|
} else if (!token && to.path !== '/login') {
|
||||||
|
next('/login')
|
||||||
|
} else if (token && to.path === '/login') {
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
5
worklog-web/src/store/index.ts
Normal file
5
worklog-web/src/store/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
export default pinia
|
||||||
49
worklog-web/src/store/user.ts
Normal file
49
worklog-web/src/store/user.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { login as loginApi, logout as logoutApi } from '@/api/auth'
|
||||||
|
import type { LoginParams, UserInfo } from '@/api/auth'
|
||||||
|
import { TOKEN_KEY, USER_INFO_KEY } from '@/utils/constants'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY))
|
||||||
|
const userInfo = ref<UserInfo | null>(
|
||||||
|
JSON.parse(localStorage.getItem(USER_INFO_KEY) || 'null')
|
||||||
|
)
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
async function login(params: LoginParams) {
|
||||||
|
const result = await loginApi(params)
|
||||||
|
token.value = result.token
|
||||||
|
userInfo.value = result.userInfo
|
||||||
|
|
||||||
|
localStorage.setItem(TOKEN_KEY, result.token)
|
||||||
|
localStorage.setItem(USER_INFO_KEY, JSON.stringify(result.userInfo))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await logoutApi()
|
||||||
|
} finally {
|
||||||
|
token.value = null
|
||||||
|
userInfo.value = null
|
||||||
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
|
localStorage.removeItem(USER_INFO_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否是管理员
|
||||||
|
function isAdmin() {
|
||||||
|
return userInfo.value?.role === 'ADMIN'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
userInfo,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
isAdmin
|
||||||
|
}
|
||||||
|
})
|
||||||
15
worklog-web/src/styles/index.css
Normal file
15
worklog-web/src/styles/index.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* 全局样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
17
worklog-web/src/utils/constants.ts
Normal file
17
worklog-web/src/utils/constants.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// API 基础配置
|
||||||
|
export const API_BASE_URL = '/wlog/api/v1'
|
||||||
|
|
||||||
|
// Token 存储 Key
|
||||||
|
export const TOKEN_KEY = 'worklog_token'
|
||||||
|
|
||||||
|
// 用户信息存储 Key
|
||||||
|
export const USER_INFO_KEY = 'worklog_user_info'
|
||||||
|
|
||||||
|
// 路由配置
|
||||||
|
export const ROUTES = {
|
||||||
|
LOGIN: '/login',
|
||||||
|
HOME: '/',
|
||||||
|
USER: '/user',
|
||||||
|
TEMPLATE: '/template',
|
||||||
|
LOG: '/log'
|
||||||
|
}
|
||||||
73
worklog-web/src/utils/request.ts
Normal file
73
worklog-web/src/utils/request.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { TOKEN_KEY } from '@/utils/constants'
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const service: AxiosInstance = axios.create({
|
||||||
|
baseURL: '/wladmin/api/v1',
|
||||||
|
timeout: 15000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY)
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
const res = response.data
|
||||||
|
|
||||||
|
// 业务成功
|
||||||
|
if (res.success) {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务失败
|
||||||
|
ElMessage.error(res.message || '请求失败')
|
||||||
|
return Promise.reject(new Error(res.message || '请求失败'))
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 401 未登录或 Token 过期
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
|
localStorage.removeItem('worklog_user_info')
|
||||||
|
window.location.href = '/login'
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.error(error.response?.data?.message || '网络错误')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 封装请求方法
|
||||||
|
export const request = {
|
||||||
|
get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.get(url, config)
|
||||||
|
},
|
||||||
|
|
||||||
|
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.post(url, data, config)
|
||||||
|
},
|
||||||
|
|
||||||
|
put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.put(url, data, config)
|
||||||
|
},
|
||||||
|
|
||||||
|
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
return service.delete(url, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default service
|
||||||
127
worklog-web/src/views/layout/index.vue
Normal file
127
worklog-web/src/views/layout/index.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="layout-container">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<el-aside width="200px" class="aside">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>工作日志</h1>
|
||||||
|
</div>
|
||||||
|
<el-menu
|
||||||
|
:default-active="currentRoute"
|
||||||
|
router
|
||||||
|
background-color="#304156"
|
||||||
|
text-color="#bfcbd9"
|
||||||
|
active-text-color="#409EFF"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/log">
|
||||||
|
<el-icon><Notebook /></el-icon>
|
||||||
|
<span>工作日志</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/template">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>模板管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/user" v-if="userStore.isAdmin()">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>人员管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
|
<!-- 主体区域 -->
|
||||||
|
<el-container>
|
||||||
|
<!-- 顶栏 -->
|
||||||
|
<el-header class="header">
|
||||||
|
<div class="header-right">
|
||||||
|
<span class="username">{{ userStore.userInfo?.name }}</span>
|
||||||
|
<el-dropdown @command="handleCommand">
|
||||||
|
<el-avatar :size="32" class="avatar">
|
||||||
|
{{ userStore.userInfo?.name?.charAt(0) }}
|
||||||
|
</el-avatar>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<el-main class="main">
|
||||||
|
<router-view />
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { Notebook, Document, User } from '@element-plus/icons-vue'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const currentRoute = computed(() => route.path)
|
||||||
|
|
||||||
|
async function handleCommand(command: string) {
|
||||||
|
if (command === 'logout') {
|
||||||
|
await userStore.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-container {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside {
|
||||||
|
background-color: #304156;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
background-color: #409EFF;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
309
worklog-web/src/views/log/index.vue
Normal file
309
worklog-web/src/views/log/index.vue
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-page">
|
||||||
|
<el-card>
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="日期范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
@change="handleDateChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<el-row class="toolbar">
|
||||||
|
<el-button type="primary" @click="handleAdd">新建日志</el-button>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table :data="tableData" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="logDate" label="日期" width="120" />
|
||||||
|
<el-table-column prop="title" label="标题" width="200" />
|
||||||
|
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||||
|
<el-table-column prop="updatedTime" label="更新时间" width="180" />
|
||||||
|
<el-table-column label="操作" fixed="right" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageNum"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="loadData"
|
||||||
|
@current-change="loadData"
|
||||||
|
class="pagination"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="日期" prop="logDate">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.logDate"
|
||||||
|
type="date"
|
||||||
|
placeholder="请选择日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
:disabled="!!form.id"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题" prop="title">
|
||||||
|
<el-input v-model="form.title" placeholder="请输入标题" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选择模板">
|
||||||
|
<el-select v-model="form.templateId" placeholder="请选择模板" clearable @change="handleTemplateChange">
|
||||||
|
<el-option
|
||||||
|
v-for="item in templateList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.templateName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="内容">
|
||||||
|
<el-input
|
||||||
|
v-model="form.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="12"
|
||||||
|
placeholder="请输入日志内容(支持Markdown格式)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 查看详情对话框 -->
|
||||||
|
<el-dialog v-model="viewDialogVisible" title="日志详情" width="700px">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="日期">{{ viewData.logDate }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="标题">{{ viewData.title }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="内容" :span="2">
|
||||||
|
<pre class="content-pre">{{ viewData.content }}</pre>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { pageMyLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
||||||
|
import type { Log } from '@/api/log'
|
||||||
|
import { listEnabledTemplates } from '@/api/template'
|
||||||
|
import type { Template } from '@/api/template'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const tableData = ref<Log[]>([])
|
||||||
|
const templateList = ref<Template[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const pageNum = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
const searchForm = reactive({
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const dateRange = ref<string[]>([])
|
||||||
|
|
||||||
|
function handleDateChange(val: string[] | null) {
|
||||||
|
if (val && val.length >= 2) {
|
||||||
|
searchForm.startDate = val[0] || ''
|
||||||
|
searchForm.endDate = val[1] || ''
|
||||||
|
} else {
|
||||||
|
searchForm.startDate = ''
|
||||||
|
searchForm.endDate = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
logDate: '',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
templateId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => form.id ? '编辑日志' : '新建日志')
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
logDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
||||||
|
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const viewDialogVisible = ref(false)
|
||||||
|
const viewData = reactive({
|
||||||
|
logDate: '',
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await pageMyLogs({
|
||||||
|
pageNum: pageNum.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
startDate: searchForm.startDate || undefined,
|
||||||
|
endDate: searchForm.endDate || undefined
|
||||||
|
})
|
||||||
|
tableData.value = result.list
|
||||||
|
total.value = result.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载模板列表
|
||||||
|
async function loadTemplates() {
|
||||||
|
templateList.value = await listEnabledTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
function handleSearch() {
|
||||||
|
pageNum.value = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
function handleReset() {
|
||||||
|
dateRange.value = []
|
||||||
|
searchForm.startDate = ''
|
||||||
|
searchForm.endDate = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
function handleAdd() {
|
||||||
|
form.id = ''
|
||||||
|
form.logDate = new Date().toISOString().split('T')[0] || ''
|
||||||
|
form.title = ''
|
||||||
|
form.content = ''
|
||||||
|
form.templateId = ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
async function handleEdit(row: Log) {
|
||||||
|
form.id = row.id
|
||||||
|
form.logDate = row.logDate
|
||||||
|
form.title = row.title
|
||||||
|
form.content = row.content || ''
|
||||||
|
form.templateId = row.templateId || ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看
|
||||||
|
function handleView(row: Log) {
|
||||||
|
viewData.logDate = row.logDate
|
||||||
|
viewData.title = row.title
|
||||||
|
viewData.content = row.content || ''
|
||||||
|
viewDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板选择变更
|
||||||
|
function handleTemplateChange(templateId: string) {
|
||||||
|
if (templateId) {
|
||||||
|
const template = templateList.value.find(t => t.id === templateId)
|
||||||
|
if (template && template.content) {
|
||||||
|
form.content = template.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
if (form.id) {
|
||||||
|
await updateLog(form.id, {
|
||||||
|
title: form.title,
|
||||||
|
content: form.content
|
||||||
|
})
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createLog({
|
||||||
|
logDate: form.logDate,
|
||||||
|
title: form.title,
|
||||||
|
content: form.content,
|
||||||
|
templateId: form.templateId || undefined
|
||||||
|
})
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
async function handleDelete(row: Log) {
|
||||||
|
await ElMessageBox.confirm('确定要删除该日志吗?', '提示', { type: 'warning' })
|
||||||
|
await deleteLog(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
loadData()
|
||||||
|
loadTemplates()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.log-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
worklog-web/src/views/login/index.vue
Normal file
104
worklog-web/src/views/login/index.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<el-card class="login-card">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="login-title">工作日志服务平台</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="0">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
prefix-icon="User"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
prefix-icon="Lock"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
style="width: 100%"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
const valid = await formRef.value?.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await userStore.login(form)
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
// 错误已在 request 中处理
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
167
worklog-web/src/views/template/index.vue
Normal file
167
worklog-web/src/views/template/index.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="template-page">
|
||||||
|
<el-card>
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<el-row class="toolbar">
|
||||||
|
<el-button type="primary" @click="handleAdd">新增模板</el-button>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table :data="tableData" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="templateName" label="模板名称" width="200" />
|
||||||
|
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||||
|
<el-table-column label="操作" fixed="right" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="primary" link @click="handleStatus(row)">
|
||||||
|
{{ row.status === 1 ? '禁用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="模板名称" prop="templateName">
|
||||||
|
<el-input v-model="form.templateName" placeholder="请输入模板名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模板内容">
|
||||||
|
<el-input
|
||||||
|
v-model="form.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder="请输入模板内容(支持Markdown格式)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { listAllTemplates, createTemplate, updateTemplate, updateTemplateStatus, deleteTemplate } from '@/api/template'
|
||||||
|
import type { Template } from '@/api/template'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const tableData = ref<Template[]>([])
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
templateName: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => form.id ? '编辑模板' : '新增模板')
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
tableData.value = await listAllTemplates()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
function handleAdd() {
|
||||||
|
form.id = ''
|
||||||
|
form.templateName = ''
|
||||||
|
form.content = ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
function handleEdit(row: Template) {
|
||||||
|
form.id = row.id
|
||||||
|
form.templateName = row.templateName
|
||||||
|
form.content = row.content || ''
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
if (form.id) {
|
||||||
|
await updateTemplate(form.id, {
|
||||||
|
templateName: form.templateName,
|
||||||
|
content: form.content
|
||||||
|
})
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createTemplate({
|
||||||
|
templateName: form.templateName,
|
||||||
|
content: form.content
|
||||||
|
})
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
async function handleStatus(row: Template) {
|
||||||
|
const newStatus = row.status === 1 ? 0 : 1
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要${newStatus === 1 ? '启用' : '禁用'}该模板吗?`,
|
||||||
|
'提示',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
await updateTemplateStatus(row.id, newStatus)
|
||||||
|
ElMessage.success('操作成功')
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
async function handleDelete(row: Template) {
|
||||||
|
await ElMessageBox.confirm('确定要删除该模板吗?', '提示', { type: 'warning' })
|
||||||
|
await deleteTemplate(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
loadData()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.template-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
298
worklog-web/src/views/user/index.vue
Normal file
298
worklog-web/src/views/user/index.vue
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-page">
|
||||||
|
<el-card>
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="姓名">
|
||||||
|
<el-input v-model="searchForm.name" placeholder="请输入姓名" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="账号">
|
||||||
|
<el-input v-model="searchForm.username" placeholder="请输入账号" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="禁用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<el-row class="toolbar" justify="space-between">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-button type="primary" @click="handleAdd">新增用户</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table :data="tableData" v-loading="loading" stripe>
|
||||||
|
<el-table-column prop="username" label="账号" width="120" />
|
||||||
|
<el-table-column prop="name" label="姓名" width="120" />
|
||||||
|
<el-table-column prop="phone" label="联系方式" width="130" />
|
||||||
|
<el-table-column prop="email" label="邮箱" width="180" />
|
||||||
|
<el-table-column prop="position" label="职位" width="120" />
|
||||||
|
<el-table-column prop="role" label="角色" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.role === 'ADMIN' ? 'danger' : 'info'">
|
||||||
|
{{ row.role === 'ADMIN' ? '管理员' : '普通用户' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||||
|
<el-table-column label="操作" fixed="right" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="handleStatus(row)"
|
||||||
|
v-if="row.role !== 'ADMIN'"
|
||||||
|
>
|
||||||
|
{{ row.status === 1 ? '禁用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
v-if="row.role !== 'ADMIN'"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageNum"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="handleSearch"
|
||||||
|
@current-change="handleSearch"
|
||||||
|
class="pagination"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="账号" prop="username" v-if="!form.id">
|
||||||
|
<el-input v-model="form.username" placeholder="请输入账号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password" v-if="!form.id">
|
||||||
|
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="联系方式">
|
||||||
|
<el-input v-model="form.phone" placeholder="请输入联系方式" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱">
|
||||||
|
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="职位">
|
||||||
|
<el-input v-model="form.position" placeholder="请输入职位" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色" v-if="!form.id">
|
||||||
|
<el-select v-model="form.role" placeholder="请选择角色">
|
||||||
|
<el-option label="普通用户" value="USER" />
|
||||||
|
<el-option label="管理员" value="ADMIN" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { pageUsers, createUser, updateUser, updateUserStatus, deleteUser } from '@/api/user'
|
||||||
|
import type { User } from '@/api/user'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const tableData = ref<User[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const pageNum = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
|
||||||
|
const searchForm = reactive({
|
||||||
|
name: '',
|
||||||
|
username: '',
|
||||||
|
status: undefined as number | undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const form = reactive({
|
||||||
|
id: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
position: '',
|
||||||
|
role: 'USER'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => form.id ? '编辑用户' : '新增用户')
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await pageUsers({
|
||||||
|
pageNum: pageNum.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
...searchForm
|
||||||
|
})
|
||||||
|
tableData.value = result.list
|
||||||
|
total.value = result.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
function handleSearch() {
|
||||||
|
pageNum.value = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.name = ''
|
||||||
|
searchForm.username = ''
|
||||||
|
searchForm.status = undefined
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
function handleAdd() {
|
||||||
|
form.id = ''
|
||||||
|
form.username = ''
|
||||||
|
form.password = ''
|
||||||
|
form.name = ''
|
||||||
|
form.phone = ''
|
||||||
|
form.email = ''
|
||||||
|
form.position = ''
|
||||||
|
form.role = 'USER'
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
function handleEdit(row: User) {
|
||||||
|
form.id = row.id
|
||||||
|
form.username = row.username
|
||||||
|
form.password = ''
|
||||||
|
form.name = row.name
|
||||||
|
form.phone = row.phone || ''
|
||||||
|
form.email = row.email || ''
|
||||||
|
form.position = row.position || ''
|
||||||
|
form.role = row.role
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
if (form.id) {
|
||||||
|
await updateUser(form.id, {
|
||||||
|
name: form.name,
|
||||||
|
phone: form.phone,
|
||||||
|
email: form.email,
|
||||||
|
position: form.position
|
||||||
|
})
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createUser({
|
||||||
|
username: form.username,
|
||||||
|
password: form.password,
|
||||||
|
name: form.name,
|
||||||
|
phone: form.phone,
|
||||||
|
email: form.email,
|
||||||
|
position: form.position,
|
||||||
|
role: form.role
|
||||||
|
})
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
async function handleStatus(row: User) {
|
||||||
|
const newStatus = row.status === 1 ? 0 : 1
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要${newStatus === 1 ? '启用' : '禁用'}该用户吗?`,
|
||||||
|
'提示',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
await updateUserStatus(row.id, newStatus)
|
||||||
|
ElMessage.success('操作成功')
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
async function handleDelete(row: User) {
|
||||||
|
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', { type: 'warning' })
|
||||||
|
await deleteUser(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
loadData()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-page {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
worklog-web/tsconfig.app.json
Normal file
17
worklog-web/tsconfig.app.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
worklog-web/tsconfig.json
Normal file
7
worklog-web/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
worklog-web/tsconfig.node.json
Normal file
26
worklog-web/tsconfig.node.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
22
worklog-web/vite.config.ts
Normal file
22
worklog-web/vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/wladmin': {
|
||||||
|
target: 'http://localhost:8200',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user