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/
|
||||
*.log
|
||||
log/
|
||||
/log/
|
||||
|
||||
# ==================== 临时文件 ====================
|
||||
*.tmp
|
||||
|
||||
@ -24,9 +24,9 @@ server {
|
||||
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;
|
||||
|
||||
# 管理后台
|
||||
# 管理后台前端静态资源
|
||||
location /admin/ {
|
||||
alias /opt/worklog/worklog-admin/dist/;
|
||||
alias /opt/worklog/worklog-web/dist/;
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
index index.html;
|
||||
|
||||
@ -37,7 +37,7 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# 移动端 H5
|
||||
# 移动端 H5 前端静态资源
|
||||
location /mobile/ {
|
||||
alias /opt/worklog/worklog-mobile/dist/;
|
||||
try_files $uri $uri/ /mobile/index.html;
|
||||
@ -50,9 +50,9 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# 后端 API 代理
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
# 管理后台 API 代理 (/wladmin/api/v1 → /wlog/api/v1)
|
||||
location /wladmin/api/ {
|
||||
proxy_pass http://127.0.0.1:8200/wlog/api/;
|
||||
|
||||
# 代理头设置
|
||||
proxy_set_header Host $host;
|
||||
@ -70,16 +70,33 @@ server {
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
proxy_busy_buffers_size 8k;
|
||||
}
|
||||
|
||||
# 移动端 API 代理 (/wlmobile/api/v1 → /wlog/api/v1)
|
||||
location /wlmobile/api/ {
|
||||
proxy_pass http://127.0.0.1:8200/wlog/api/;
|
||||
|
||||
# WebSocket 支持(如需要)
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# 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 文档(生产环境建议关闭或限制访问)
|
||||
location /swagger-ui.html {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
location /wlog/swagger-ui.html {
|
||||
proxy_pass http://127.0.0.1:8200;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@ -90,8 +107,8 @@ server {
|
||||
}
|
||||
|
||||
# 健康检查接口
|
||||
location /api/v1/health {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
location /wlog/api/v1/health {
|
||||
proxy_pass http://127.0.0.1:8200;
|
||||
proxy_set_header Host $host;
|
||||
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