feat: init fronted
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "item-manager-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"element-plus": "^2.11.2",
|
||||||
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"vue": "^3.2.13",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.16",
|
||||||
|
"@babel/eslint-parser": "^7.12.16",
|
||||||
|
"@vue/cli-plugin-babel": "~5.0.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||||
|
"@vue/cli-service": "~5.0.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@babel/eslint-parser"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"vue/multi-word-component-names": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not ie 11"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<svg id="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="62" height="66" viewBox="0 0 62 66">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #f8d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-1, .cls-2, .cls-3 {
|
||||||
|
fill-rule: evenodd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #bab5ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: url(#linear-gradient);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="linear-gradient" x1="19.41" y1="66" x2="44.59" y2="12" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#bab5ec"/>
|
||||||
|
<stop offset="1" stop-color="#f1b7bf"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path id="cls-1" class="cls-1" d="M0,17L30,0V39L0,57V17Z"/>
|
||||||
|
<path id="cls-2" class="cls-2" d="M33,39L62,56V17L33,0V39Z"/>
|
||||||
|
<path id="cls-3" class="cls-3" d="M18,28L46,12V49L18,66V28Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 826 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.svg">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
<template>
|
||||||
|
<div class="connection-test">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>前后端连接测试</span>
|
||||||
|
</template>
|
||||||
|
<div v-if="loading">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
正在测试连接...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-result
|
||||||
|
:icon="connectionStatus.success ? 'success' : 'error'"
|
||||||
|
:title="connectionStatus.title"
|
||||||
|
:sub-title="connectionStatus.message"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="testConnection">重新测试</el-button>
|
||||||
|
</template>
|
||||||
|
</el-result>
|
||||||
|
|
||||||
|
<div v-if="connectionStatus.success && apiData">
|
||||||
|
<h3>API数据示例:</h3>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="API状态">正常</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="响应时间">{{ responseTime }}ms</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="数据格式">JSON</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="CORS配置">已启用</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { healthService } from '../services/api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ConnectionTest',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
connectionStatus: {
|
||||||
|
success: false,
|
||||||
|
title: '未测试',
|
||||||
|
message: '点击测试按钮检查连接'
|
||||||
|
},
|
||||||
|
apiData: null,
|
||||||
|
responseTime: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.testConnection()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async testConnection() {
|
||||||
|
this.loading = true
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await healthService.checkBackendConnection()
|
||||||
|
this.responseTime = Date.now() - startTime
|
||||||
|
|
||||||
|
this.connectionStatus = {
|
||||||
|
success: true,
|
||||||
|
title: '连接成功!',
|
||||||
|
message: '前后端连接正常,API可以正常访问'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apiData = response.data
|
||||||
|
ElMessage.success('后端连接测试成功')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.responseTime = Date.now() - startTime
|
||||||
|
|
||||||
|
if (error.code === 'ERR_NETWORK') {
|
||||||
|
this.connectionStatus = {
|
||||||
|
success: false,
|
||||||
|
title: '网络错误',
|
||||||
|
message: '无法连接到后端服务器,请检查Django服务器是否运行在 http://localhost:8000'
|
||||||
|
}
|
||||||
|
} else if (error.response?.status === 404) {
|
||||||
|
this.connectionStatus = {
|
||||||
|
success: false,
|
||||||
|
title: 'API路径错误',
|
||||||
|
message: '后端服务器正在运行,但API路径配置有误'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.connectionStatus = {
|
||||||
|
success: false,
|
||||||
|
title: '连接失败',
|
||||||
|
message: `错误: ${error.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('连接测试失败:', error)
|
||||||
|
ElMessage.error('后端连接测试失败')
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.connection-test {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-loading {
|
||||||
|
animation: rotating 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotating {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hello">
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
<p>
|
||||||
|
For a guide and recipes on how to configure / customize this project,<br>
|
||||||
|
check out the
|
||||||
|
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||||
|
</p>
|
||||||
|
<h3>Installed CLI Plugins</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||||
|
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||||
|
</ul>
|
||||||
|
<h3>Essential Links</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||||
|
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||||
|
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||||
|
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||||
|
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||||
|
</ul>
|
||||||
|
<h3>Ecosystem</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||||
|
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||||
|
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||||
|
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||||
|
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HelloWorld',
|
||||||
|
props: {
|
||||||
|
msg: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
h3 {
|
||||||
|
margin: 40px 0 0;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #42b983;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册Element Plus图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Login from '../views/Login.vue'
|
||||||
|
import ItemList from '../views/ItemList.vue'
|
||||||
|
import ItemDetail from '../views/ItemDetail.vue'
|
||||||
|
import ItemUsage from '../views/ItemUsage.vue'
|
||||||
|
import Dashboard from '../views/Dashboard.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: 'Login',
|
||||||
|
component: Login,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: Dashboard
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/items',
|
||||||
|
name: 'ItemList',
|
||||||
|
component: ItemList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/items/:id',
|
||||||
|
name: 'ItemDetail',
|
||||||
|
component: ItemDetail,
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/usage',
|
||||||
|
name: 'ItemUsage',
|
||||||
|
component: ItemUsage
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://localhost:8000/api'
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
console.log('API请求:', config.method?.toUpperCase(), config.url)
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
console.log('API响应:', response.status, response.config.url)
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('API错误:', error.response?.status, error.response?.data)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const itemService = {
|
||||||
|
// 获取所有物品
|
||||||
|
getAllItems() {
|
||||||
|
return apiClient.get('/items/')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取物品详情
|
||||||
|
getItemDetail(id) {
|
||||||
|
return apiClient.get(`/items/${id}/`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建新物品
|
||||||
|
createItem(item) {
|
||||||
|
return apiClient.post('/items/', item)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新物品
|
||||||
|
updateItem(id, item) {
|
||||||
|
return apiClient.put(`/items/${id}/`, item)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除物品
|
||||||
|
deleteItem(id) {
|
||||||
|
return apiClient.delete(`/items/${id}/`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取可用物品
|
||||||
|
getAvailableItems() {
|
||||||
|
return apiClient.get('/items/available/')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取使用中的物品
|
||||||
|
getItemsInUse() {
|
||||||
|
return apiClient.get('/items/in_use/')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 借用物品
|
||||||
|
borrowItem(itemId, borrowData) {
|
||||||
|
return apiClient.post(`/items/${itemId}/borrow/`, borrowData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 归还物品
|
||||||
|
returnItem(itemId, returnData) {
|
||||||
|
return apiClient.post(`/items/${itemId}/return_item/`, returnData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usageService = {
|
||||||
|
// 获取所有使用记录
|
||||||
|
getAllUsages() {
|
||||||
|
return apiClient.get('/usages/')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前使用中的记录
|
||||||
|
getCurrentUsages() {
|
||||||
|
return apiClient.get('/usages/current/')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 根据用户获取使用记录
|
||||||
|
getUserUsages(userId) {
|
||||||
|
return apiClient.get(`/usages/by_user/?user_id=${userId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建使用记录
|
||||||
|
createUsage(usage) {
|
||||||
|
return apiClient.post('/usages/', usage)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新使用记录
|
||||||
|
updateUsage(id, usage) {
|
||||||
|
return apiClient.put(`/usages/${id}/`, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const categoryService = {
|
||||||
|
// 获取所有类别
|
||||||
|
getAllCategories() {
|
||||||
|
return apiClient.get('/categories/')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建新类别
|
||||||
|
createCategory(category) {
|
||||||
|
return apiClient.post('/categories/', category)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新类别
|
||||||
|
updateCategory(id, category) {
|
||||||
|
return apiClient.put(`/categories/${id}/`, category)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除类别
|
||||||
|
deleteCategory(id) {
|
||||||
|
return apiClient.delete(`/categories/${id}/`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userService = {
|
||||||
|
// 获取所有用户
|
||||||
|
getAllUsers() {
|
||||||
|
return apiClient.get('/users/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 健康检查API
|
||||||
|
export const healthService = {
|
||||||
|
checkBackendConnection() {
|
||||||
|
return apiClient.get('/items/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default apiClient
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
<template>
|
||||||
|
<el-header>
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="logo">爱特工作室物品管理系统</h1>
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
:default-active="$route.path"
|
||||||
|
router
|
||||||
|
class="nav-menu"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<el-icon><House /></el-icon>
|
||||||
|
首页
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/items">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
物品管理
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/usage">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
使用记录
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<div class="dashboard">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon available">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ stats.available }}</h3>
|
||||||
|
<p>可用物品</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon in-use">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ stats.inUse }}</h3>
|
||||||
|
<p>使用中</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon total">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ stats.total }}</h3>
|
||||||
|
<p>总物品数</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon maintenance">
|
||||||
|
<el-icon><Tools /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ stats.maintenance }}</h3>
|
||||||
|
<p>维护中</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>当前使用中的物品</span>
|
||||||
|
</template>
|
||||||
|
<el-table :data="currentUsages" style="width: 100%" max-height="300">
|
||||||
|
<el-table-column prop="item.name" label="物品名称" />
|
||||||
|
<el-table-column prop="user.username" label="使用者" />
|
||||||
|
<el-table-column prop="start_time" label="开始时间">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.start_time) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="purpose" label="使用目的" />
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>最新添加的物品</span>
|
||||||
|
</template>
|
||||||
|
<el-table :data="recentItems" style="width: 100%" max-height="300">
|
||||||
|
<el-table-column prop="name" label="物品名称" />
|
||||||
|
<el-table-column prop="category" label="类别" />
|
||||||
|
<el-table-column prop="status" label="状态">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)">
|
||||||
|
{{ getStatusText(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="created_at" label="添加时间">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { itemService, usageService } from '../services/api'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Dashboard',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
stats: {
|
||||||
|
total: 0,
|
||||||
|
available: 0,
|
||||||
|
inUse: 0,
|
||||||
|
maintenance: 0
|
||||||
|
},
|
||||||
|
currentUsages: [],
|
||||||
|
recentItems: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.loadDashboardData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadDashboardData() {
|
||||||
|
try {
|
||||||
|
// 获取统计数据
|
||||||
|
const itemsResponse = await itemService.getAllItems()
|
||||||
|
const items = itemsResponse.data
|
||||||
|
|
||||||
|
this.stats.total = items.length
|
||||||
|
this.stats.available = items.filter(item => item.status === 'available').length
|
||||||
|
this.stats.inUse = items.filter(item => item.status === 'in_use').length
|
||||||
|
this.stats.maintenance = items.filter(item => item.status === 'maintenance').length
|
||||||
|
|
||||||
|
// 获取最新物品
|
||||||
|
this.recentItems = items.slice(0, 5)
|
||||||
|
|
||||||
|
// 获取当前使用记录
|
||||||
|
const usagesResponse = await usageService.getCurrentUsages()
|
||||||
|
this.currentUsages = usagesResponse.data.slice(0, 5)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载仪表盘数据失败:', error)
|
||||||
|
this.$message.error('加载数据失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(dateString) {
|
||||||
|
return moment(dateString).format('YYYY-MM-DD HH:mm')
|
||||||
|
},
|
||||||
|
getStatusType(status) {
|
||||||
|
const typeMap = {
|
||||||
|
'available': 'success',
|
||||||
|
'in_use': 'warning',
|
||||||
|
'maintenance': 'info',
|
||||||
|
'damaged': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
},
|
||||||
|
getStatusText(status) {
|
||||||
|
const textMap = {
|
||||||
|
'available': '可用',
|
||||||
|
'in_use': '使用中',
|
||||||
|
'maintenance': '维护中',
|
||||||
|
'damaged': '损坏'
|
||||||
|
}
|
||||||
|
return textMap[status] || '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.available {
|
||||||
|
background-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.in-use {
|
||||||
|
background-color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.total {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon.maintenance {
|
||||||
|
background-color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info p {
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<el-header>
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="logo">爱特工作室物品管理系统</h1>
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
:default-active="$route.path"
|
||||||
|
router
|
||||||
|
class="nav-menu"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<el-icon><House /></el-icon>
|
||||||
|
首页
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/items">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
物品管理
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/usage">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
使用记录
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<div class="item-detail">
|
||||||
|
<div class="detail-header">
|
||||||
|
<el-button @click="$router.go(-1)" style="margin-bottom: 20px;">
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
返回
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card v-loading="loading">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>物品详情</span>
|
||||||
|
<el-button type="primary" @click="editMode = !editMode">
|
||||||
|
{{ editMode ? '取消编辑' : '编辑' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="item">
|
||||||
|
<el-form :model="editableItem" label-width="120px" :disabled="!editMode">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="物品名称">
|
||||||
|
<el-input v-model="editableItem.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="序列号">
|
||||||
|
<el-input v-model="editableItem.serial_number" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类别">
|
||||||
|
<el-input v-model="editableItem.category" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="editableItem.status">
|
||||||
|
<el-option label="可用" value="available" />
|
||||||
|
<el-option label="使用中" value="in_use" />
|
||||||
|
<el-option label="维护中" value="maintenance" />
|
||||||
|
<el-option label="损坏" value="damaged" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="位置">
|
||||||
|
<el-input v-model="editableItem.location" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="购买日期">
|
||||||
|
<el-date-picker v-model="editableItem.purchase_date" type="date" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="价值">
|
||||||
|
<el-input-number v-model="editableItem.value" :precision="2" :min="0" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间">
|
||||||
|
<el-input :value="formatDate(item.created_at)" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="更新时间">
|
||||||
|
<el-input :value="formatDate(item.updated_at)" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input type="textarea" v-model="editableItem.description" :rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div v-if="editMode" style="text-align: center; margin-top: 20px;">
|
||||||
|
<el-button @click="editMode = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveChanges">保存修改</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前使用者信息 -->
|
||||||
|
<el-card v-if="item.current_user" style="margin-top: 20px;">
|
||||||
|
<template #header>
|
||||||
|
<span>当前使用者</span>
|
||||||
|
</template>
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item label="使用者">{{ item.current_user.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="姓名">{{ item.current_user.first_name }} {{ item.current_user.last_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">{{ item.current_user.email }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 使用历史 -->
|
||||||
|
<el-card style="margin-top: 20px;">
|
||||||
|
<template #header>
|
||||||
|
<span>使用历史</span>
|
||||||
|
</template>
|
||||||
|
<el-table :data="item.usage_history" style="width: 100%">
|
||||||
|
<el-table-column prop="user.username" label="使用者" />
|
||||||
|
<el-table-column prop="start_time" label="开始时间">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.start_time) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="end_time" label="结束时间">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.end_time ? formatDate(scope.row.end_time) : '使用中' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="purpose" label="使用目的" />
|
||||||
|
<el-table-column prop="is_returned" label="状态">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.is_returned ? 'success' : 'warning'">
|
||||||
|
{{ scope.row.is_returned ? '已归还' : '使用中' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" @click="showUsageDetail(scope.row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 使用记录详情对话框 -->
|
||||||
|
<el-dialog v-model="showUsageDialog" title="使用记录详情" width="600px">
|
||||||
|
<div v-if="selectedUsage">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="使用者">{{ selectedUsage.user.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="使用目的">{{ selectedUsage.purpose }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开始时间">{{ formatDate(selectedUsage.start_time) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结束时间">
|
||||||
|
{{ selectedUsage.end_time ? formatDate(selectedUsage.end_time) : '使用中' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="使用前状况">{{ selectedUsage.condition_before || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="使用后状况">{{ selectedUsage.condition_after || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ selectedUsage.notes || '无' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { itemService } from '../services/api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ItemDetail',
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
item: null,
|
||||||
|
editableItem: {},
|
||||||
|
loading: false,
|
||||||
|
editMode: false,
|
||||||
|
showUsageDialog: false,
|
||||||
|
selectedUsage: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.loadItem()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadItem() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const response = await itemService.getItemDetail(this.id)
|
||||||
|
this.item = response.data
|
||||||
|
this.editableItem = { ...this.item }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载物品详情失败:', error)
|
||||||
|
ElMessage.error('加载物品详情失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveChanges() {
|
||||||
|
try {
|
||||||
|
await itemService.updateItem(this.id, this.editableItem)
|
||||||
|
ElMessage.success('修改保存成功')
|
||||||
|
this.editMode = false
|
||||||
|
await this.loadItem()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存修改失败:', error)
|
||||||
|
ElMessage.error('保存修改失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showUsageDetail(usage) {
|
||||||
|
this.selectedUsage = usage
|
||||||
|
this.showUsageDialog = true
|
||||||
|
},
|
||||||
|
formatDate(dateString) {
|
||||||
|
return moment(dateString).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item-detail {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
<template>
|
||||||
|
<el-header>
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="logo">爱特工作室物品管理系统</h1>
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
:default-active="$route.path"
|
||||||
|
router
|
||||||
|
class="nav-menu"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<el-icon><House /></el-icon>
|
||||||
|
首页
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/items">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
物品管理
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/usage">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
使用记录
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<div class="item-list">
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button type="primary" @click="showAddDialog = true">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加物品
|
||||||
|
</el-button>
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索物品名称或序列号"
|
||||||
|
style="width: 300px;"
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-select v-model="statusFilter" placeholder="状态筛选" style="margin-left: 10px;" @change="handleFilter">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="可用" value="available" />
|
||||||
|
<el-option label="使用中" value="in_use" />
|
||||||
|
<el-option label="维护中" value="maintenance" />
|
||||||
|
<el-option label="损坏" value="damaged" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="filteredItems" style="width: 100%" v-loading="loading">
|
||||||
|
<el-table-column prop="name" label="物品名称" />
|
||||||
|
<el-table-column prop="serial_number" label="序列号" />
|
||||||
|
<el-table-column prop="category" label="类别" />
|
||||||
|
<el-table-column prop="status" label="状态">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)">
|
||||||
|
{{ getStatusText(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="current_user" label="当前使用者">
|
||||||
|
<template #default="scope">
|
||||||
|
<span v-if="scope.row.current_user">
|
||||||
|
{{ scope.row.current_user.username }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="location" label="位置" />
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" @click="viewItem(scope.row.id)">
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.status === 'available'"
|
||||||
|
size="small"
|
||||||
|
type="warning"
|
||||||
|
@click="borrowItem(scope.row)"
|
||||||
|
>
|
||||||
|
借用
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.status === 'in_use'"
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
@click="returnItem(scope.row)"
|
||||||
|
>
|
||||||
|
归还
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="editItem(scope.row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 添加物品对话框 -->
|
||||||
|
<el-dialog v-model="showAddDialog" title="添加物品" width="600px">
|
||||||
|
<el-form :model="newItem" label-width="100px" :rules="itemRules" ref="itemForm">
|
||||||
|
<el-form-item label="物品名称" prop="name">
|
||||||
|
<el-input v-model="newItem.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="序列号" prop="serial_number">
|
||||||
|
<el-input v-model="newItem.serial_number" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类别" prop="category">
|
||||||
|
<el-input v-model="newItem.category" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input type="textarea" v-model="newItem.description" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="位置">
|
||||||
|
<el-input v-model="newItem.location" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="价值">
|
||||||
|
<el-input-number v-model="newItem.value" :precision="2" :min="0" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="购买日期">
|
||||||
|
<el-date-picker v-model="newItem.purchase_date" type="date" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showAddDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveItem">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 借用物品对话框 -->
|
||||||
|
<el-dialog v-model="showBorrowDialog" title="借用物品" width="500px">
|
||||||
|
<el-form :model="borrowForm" label-width="100px">
|
||||||
|
<el-form-item label="使用者">
|
||||||
|
<el-select v-model="borrowForm.user_id" placeholder="请选择使用者">
|
||||||
|
<el-option
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:label="user.username"
|
||||||
|
:value="user.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="使用目的">
|
||||||
|
<el-input v-model="borrowForm.purpose" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="使用前状况">
|
||||||
|
<el-input v-model="borrowForm.condition_before" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input type="textarea" v-model="borrowForm.notes" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showBorrowDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmBorrow">确认借用</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 归还物品对话框 -->
|
||||||
|
<el-dialog v-model="showReturnDialog" title="归还物品" width="500px">
|
||||||
|
<el-form :model="returnForm" label-width="100px">
|
||||||
|
<el-form-item label="使用后状况">
|
||||||
|
<el-input v-model="returnForm.condition_after" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="归还备注">
|
||||||
|
<el-input type="textarea" v-model="returnForm.return_notes" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showReturnDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmReturn">确认归还</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { itemService, userService } from '../services/api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ItemList',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
users: [],
|
||||||
|
loading: false,
|
||||||
|
searchKeyword: '',
|
||||||
|
statusFilter: '',
|
||||||
|
showAddDialog: false,
|
||||||
|
showBorrowDialog: false,
|
||||||
|
showReturnDialog: false,
|
||||||
|
currentItem: null,
|
||||||
|
newItem: {
|
||||||
|
name: '',
|
||||||
|
serial_number: '',
|
||||||
|
category: '',
|
||||||
|
description: '',
|
||||||
|
location: '',
|
||||||
|
value: null,
|
||||||
|
purchase_date: null
|
||||||
|
},
|
||||||
|
borrowForm: {
|
||||||
|
user_id: null,
|
||||||
|
purpose: '',
|
||||||
|
condition_before: '',
|
||||||
|
notes: ''
|
||||||
|
},
|
||||||
|
returnForm: {
|
||||||
|
condition_after: '',
|
||||||
|
return_notes: ''
|
||||||
|
},
|
||||||
|
itemRules: {
|
||||||
|
name: [{ required: true, message: '请输入物品名称', trigger: 'blur' }],
|
||||||
|
serial_number: [{ required: true, message: '请输入序列号', trigger: 'blur' }],
|
||||||
|
category: [{ required: true, message: '请输入类别', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredItems() {
|
||||||
|
let filtered = this.items
|
||||||
|
|
||||||
|
if (this.searchKeyword) {
|
||||||
|
filtered = filtered.filter(item =>
|
||||||
|
item.name.toLowerCase().includes(this.searchKeyword.toLowerCase()) ||
|
||||||
|
item.serial_number.toLowerCase().includes(this.searchKeyword.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusFilter) {
|
||||||
|
filtered = filtered.filter(item => item.status === this.statusFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.loadItems()
|
||||||
|
await this.loadUsers()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadItems() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const response = await itemService.getAllItems()
|
||||||
|
this.items = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载物品列表失败:', error)
|
||||||
|
ElMessage.error('加载物品列表失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadUsers() {
|
||||||
|
try {
|
||||||
|
const response = await userService.getAllUsers()
|
||||||
|
this.users = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户列表失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
// 搜索逻辑在computed中处理
|
||||||
|
},
|
||||||
|
handleFilter() {
|
||||||
|
// 筛选逻辑在computed中处理
|
||||||
|
},
|
||||||
|
viewItem(id) {
|
||||||
|
this.$router.push(`/items/${id}`)
|
||||||
|
},
|
||||||
|
editItem(item) {
|
||||||
|
// TODO: 实现编辑功能
|
||||||
|
ElMessage.info('编辑功能开发中')
|
||||||
|
},
|
||||||
|
borrowItem(item) {
|
||||||
|
this.currentItem = item
|
||||||
|
this.borrowForm = {
|
||||||
|
user_id: null,
|
||||||
|
purpose: '',
|
||||||
|
condition_before: '',
|
||||||
|
notes: ''
|
||||||
|
}
|
||||||
|
this.showBorrowDialog = true
|
||||||
|
},
|
||||||
|
returnItem(item) {
|
||||||
|
this.currentItem = item
|
||||||
|
this.returnForm = {
|
||||||
|
condition_after: '',
|
||||||
|
return_notes: ''
|
||||||
|
}
|
||||||
|
this.showReturnDialog = true
|
||||||
|
},
|
||||||
|
async saveItem() {
|
||||||
|
try {
|
||||||
|
await this.$refs.itemForm.validate()
|
||||||
|
await itemService.createItem(this.newItem)
|
||||||
|
ElMessage.success('物品添加成功')
|
||||||
|
this.showAddDialog = false
|
||||||
|
this.newItem = {
|
||||||
|
name: '',
|
||||||
|
serial_number: '',
|
||||||
|
category: '',
|
||||||
|
description: '',
|
||||||
|
location: '',
|
||||||
|
value: null,
|
||||||
|
purchase_date: null
|
||||||
|
}
|
||||||
|
await this.loadItems()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加物品失败:', error)
|
||||||
|
ElMessage.error('添加物品失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirmBorrow() {
|
||||||
|
if (!this.borrowForm.user_id) {
|
||||||
|
ElMessage.error('请选择使用者')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await itemService.borrowItem(this.currentItem.id, this.borrowForm)
|
||||||
|
ElMessage.success('借用成功')
|
||||||
|
this.showBorrowDialog = false
|
||||||
|
await this.loadItems()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('借用失败:', error)
|
||||||
|
ElMessage.error('借用失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirmReturn() {
|
||||||
|
try {
|
||||||
|
await itemService.returnItem(this.currentItem.id, this.returnForm)
|
||||||
|
ElMessage.success('归还成功')
|
||||||
|
this.showReturnDialog = false
|
||||||
|
await this.loadItems()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('归还失败:', error)
|
||||||
|
ElMessage.error('归还失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getStatusType(status) {
|
||||||
|
const typeMap = {
|
||||||
|
'available': 'success',
|
||||||
|
'in_use': 'warning',
|
||||||
|
'maintenance': 'info',
|
||||||
|
'damaged': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
},
|
||||||
|
getStatusText(status) {
|
||||||
|
const textMap = {
|
||||||
|
'available': '可用',
|
||||||
|
'in_use': '使用中',
|
||||||
|
'maintenance': '维护中',
|
||||||
|
'damaged': '损坏'
|
||||||
|
}
|
||||||
|
return textMap[status] || '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item-list {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
<template>
|
||||||
|
<el-header>
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="logo">爱特工作室物品管理系统</h1>
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
:default-active="$route.path"
|
||||||
|
router
|
||||||
|
class="nav-menu"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<el-icon><House /></el-icon>
|
||||||
|
首页
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/items">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
物品管理
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/usage">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
使用记录
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<div class="item-usage">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h2>使用记录</h2>
|
||||||
|
<div class="filters">
|
||||||
|
<el-select v-model="statusFilter" placeholder="状态筛选" @change="handleFilter">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="使用中" value="false" />
|
||||||
|
<el-option label="已归还" value="true" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="userFilter" placeholder="用户筛选" @change="handleFilter">
|
||||||
|
<el-option label="全部用户" value="" />
|
||||||
|
<el-option
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:label="user.username"
|
||||||
|
:value="user.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
@change="handleFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card>
|
||||||
|
<el-table :data="filteredUsages" style="width: 100%" v-loading="loading">
|
||||||
|
<el-table-column prop="item.name" label="物品名称" />
|
||||||
|
<el-table-column prop="item.serial_number" label="序列号" />
|
||||||
|
<el-table-column prop="user.username" label="使用者" />
|
||||||
|
<el-table-column prop="start_time" label="开始时间" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.start_time) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="end_time" label="结束时间" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.end_time ? formatDate(scope.row.end_time) : '使用中' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="purpose" label="使用目的" />
|
||||||
|
<el-table-column prop="is_returned" label="状态" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.is_returned ? 'success' : 'warning'">
|
||||||
|
{{ scope.row.is_returned ? '已归还' : '使用中' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="使用时长" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ calculateDuration(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" @click="showUsageDetail(scope.row)">
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="totalUsages"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 使用记录详情对话框 -->
|
||||||
|
<el-dialog v-model="showDetailDialog" title="使用记录详情" width="700px">
|
||||||
|
<div v-if="selectedUsage">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<h3>物品信息</h3>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="物品名称">{{ selectedUsage.item.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="序列号">{{ selectedUsage.item.serial_number }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="类别">{{ selectedUsage.item.category }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<h3>使用者信息</h3>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="用户名">{{ selectedUsage.user.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="姓名">
|
||||||
|
{{ selectedUsage.user.first_name }} {{ selectedUsage.user.last_name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">{{ selectedUsage.user.email }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 20px;">使用详情</h3>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="使用目的">{{ selectedUsage.purpose }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开始时间">{{ formatDate(selectedUsage.start_time) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结束时间">
|
||||||
|
{{ selectedUsage.end_time ? formatDate(selectedUsage.end_time) : '使用中' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="使用时长">{{ calculateDuration(selectedUsage) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="使用前状况">{{ selectedUsage.condition_before || '无记录' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="使用后状况">{{ selectedUsage.condition_after || '无记录' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ selectedUsage.notes || '无' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态" :span="2">
|
||||||
|
<el-tag :type="selectedUsage.is_returned ? 'success' : 'warning'">
|
||||||
|
{{ selectedUsage.is_returned ? '已归还' : '使用中' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 统计信息卡片 -->
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<el-statistic title="总使用记录" :value="totalUsages" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<el-statistic title="当前使用中" :value="currentUsageCount" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<el-statistic title="今日借用" :value="todayUsageCount" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<el-statistic title="本月借用" :value="monthUsageCount" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { usageService, userService } from '../services/api'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ItemUsage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
usages: [],
|
||||||
|
users: [],
|
||||||
|
loading: false,
|
||||||
|
showDetailDialog: false,
|
||||||
|
selectedUsage: null,
|
||||||
|
statusFilter: '',
|
||||||
|
userFilter: '',
|
||||||
|
dateRange: null,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
totalUsages: 0,
|
||||||
|
currentUsageCount: 0,
|
||||||
|
todayUsageCount: 0,
|
||||||
|
monthUsageCount: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredUsages() {
|
||||||
|
let filtered = this.usages
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (this.statusFilter !== '') {
|
||||||
|
const isReturned = this.statusFilter === 'true'
|
||||||
|
filtered = filtered.filter(usage => usage.is_returned === isReturned)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户筛选
|
||||||
|
if (this.userFilter) {
|
||||||
|
filtered = filtered.filter(usage => usage.user.id === parseInt(this.userFilter))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期范围筛选
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
const startDate = moment(this.dateRange[0]).startOf('day')
|
||||||
|
const endDate = moment(this.dateRange[1]).endOf('day')
|
||||||
|
filtered = filtered.filter(usage => {
|
||||||
|
const usageDate = moment(usage.start_time)
|
||||||
|
return usageDate.isBetween(startDate, endDate, null, '[]')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData() {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadUsages(),
|
||||||
|
this.loadUsers(),
|
||||||
|
this.loadStatistics()
|
||||||
|
])
|
||||||
|
},
|
||||||
|
async loadUsages() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const response = await usageService.getAllUsages()
|
||||||
|
this.usages = response.data
|
||||||
|
this.totalUsages = this.usages.length
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载使用记录失败:', error)
|
||||||
|
ElMessage.error('加载使用记录失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadUsers() {
|
||||||
|
try {
|
||||||
|
const response = await userService.getAllUsers()
|
||||||
|
this.users = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户列表失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadStatistics() {
|
||||||
|
try {
|
||||||
|
// 计算统计数据
|
||||||
|
const currentUsages = await usageService.getCurrentUsages()
|
||||||
|
this.currentUsageCount = currentUsages.data.length
|
||||||
|
|
||||||
|
const today = moment().startOf('day')
|
||||||
|
const thisMonth = moment().startOf('month')
|
||||||
|
|
||||||
|
this.todayUsageCount = this.usages.filter(usage =>
|
||||||
|
moment(usage.start_time).isSameOrAfter(today)
|
||||||
|
).length
|
||||||
|
|
||||||
|
this.monthUsageCount = this.usages.filter(usage =>
|
||||||
|
moment(usage.start_time).isSameOrAfter(thisMonth)
|
||||||
|
).length
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载统计数据失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleFilter() {
|
||||||
|
// 筛选逻辑在computed中处理
|
||||||
|
},
|
||||||
|
handleSizeChange(size) {
|
||||||
|
this.pageSize = size
|
||||||
|
this.currentPage = 1
|
||||||
|
},
|
||||||
|
handleCurrentChange(page) {
|
||||||
|
this.currentPage = page
|
||||||
|
},
|
||||||
|
showUsageDetail(usage) {
|
||||||
|
this.selectedUsage = usage
|
||||||
|
this.showDetailDialog = true
|
||||||
|
},
|
||||||
|
formatDate(dateString) {
|
||||||
|
return moment(dateString).format('YYYY-MM-DD HH:mm')
|
||||||
|
},
|
||||||
|
calculateDuration(usage) {
|
||||||
|
const start = moment(usage.start_time)
|
||||||
|
const end = usage.end_time ? moment(usage.end_time) : moment()
|
||||||
|
const duration = moment.duration(end.diff(start))
|
||||||
|
|
||||||
|
if (duration.asDays() >= 1) {
|
||||||
|
return `${Math.floor(duration.asDays())}天${duration.hours()}小时`
|
||||||
|
} else if (duration.asHours() >= 1) {
|
||||||
|
return `${Math.floor(duration.asHours())}小时${duration.minutes()}分钟`
|
||||||
|
} else {
|
||||||
|
return `${Math.floor(duration.asMinutes())}分钟`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item-usage {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<!-- 全页面背景轮播图 -->
|
||||||
|
<div class="background-carousel">
|
||||||
|
<div class="carousel-overlay"></div>
|
||||||
|
<div class="carousel-container">
|
||||||
|
<div
|
||||||
|
v-for="(image, index) in backgroundImages"
|
||||||
|
:key="index"
|
||||||
|
:class="['carousel-slide', { active: currentSlide === index }]"
|
||||||
|
:style="{ backgroundImage: `url(${image})` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="login-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="system-title">爱特工作室物品管理及财务管理系统</h1>
|
||||||
|
<div class="favicon-container">
|
||||||
|
<img src="../../public/favicon.svg" alt="网站图标" class="favicon">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体内容 -->
|
||||||
|
<div class="login-main">
|
||||||
|
<!-- 右侧登录表单 -->
|
||||||
|
<div class="login-form-container">
|
||||||
|
<div class="login-form-wrapper">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>欢迎来到爱特工作室</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
:model="loginForm"
|
||||||
|
:rules="formRules"
|
||||||
|
ref="loginFormRef"
|
||||||
|
class="login-form"
|
||||||
|
@submit.prevent="handleLogin"
|
||||||
|
>
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
placeholder="账户"
|
||||||
|
size="large"
|
||||||
|
prefix-icon="User"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="密码"
|
||||||
|
size="large"
|
||||||
|
prefix-icon="Lock"
|
||||||
|
show-password
|
||||||
|
class="form-input"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="login-button"
|
||||||
|
:loading="isLoading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
{{ isLoading ? '登录中...' : '登录' }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 网站免责说明 -->
|
||||||
|
<div class="disclaimer">
|
||||||
|
<p class="disclaimer-text">
|
||||||
|
本系统仅供爱特工作室内部使用。使用本系统即表示您同意遵守相关规定,
|
||||||
|
工作室对系统使用过程中产生的任何问题不承担法律责任。
|
||||||
|
如有疑问,请联系管理员。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Login',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loginForm: {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
formRules: {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入账户', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 20, message: '账户长度在 2 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
currentSlide: 0,
|
||||||
|
backgroundImages: [
|
||||||
|
'../../_images/background1.jpg',
|
||||||
|
'../../_images/background2.jpg',
|
||||||
|
],
|
||||||
|
slideTimer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initializeCarousel()
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.slideTimer) {
|
||||||
|
clearInterval(this.slideTimer)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initializeCarousel() {
|
||||||
|
// 启动轮播定时器
|
||||||
|
this.slideTimer = setInterval(() => {
|
||||||
|
this.nextSlide()
|
||||||
|
}, 5000) // 每5秒切换一张图片
|
||||||
|
},
|
||||||
|
|
||||||
|
nextSlide() {
|
||||||
|
this.currentSlide = (this.currentSlide + 1) % this.backgroundImages.length
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLogin() {
|
||||||
|
try {
|
||||||
|
// 表单验证
|
||||||
|
await this.$refs.loginFormRef.validate()
|
||||||
|
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
// 模拟登录API调用
|
||||||
|
await this.performLogin()
|
||||||
|
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
|
// 跳转到主页
|
||||||
|
await this.$router.push('/')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
if (error !== 'validation failed') {
|
||||||
|
ElMessage.error('登录失败,请检查账户和密码')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async performLogin() {
|
||||||
|
// 模拟API调用延迟
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// 简单的模拟验证
|
||||||
|
if (this.loginForm.username && this.loginForm.password) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(new Error('账户或密码错误'))
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 重置所有元素的盒模型和边距 */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden; /* 隐藏整体滚动条 */
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: fixed; /* 改为fixed避免滚动 */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全页面背景轮播图样式 */
|
||||||
|
.background-carousel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.3) 100%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-slide {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 2s ease-in-out;
|
||||||
|
transform: scale(1.02); /* 减小缩放避免溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-slide.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header 样式优化 */
|
||||||
|
.login-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.08) 100%);
|
||||||
|
backdrop-filter: blur(15px) saturate(150%);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
flex-shrink: 0; /* 防止Header被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 30px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 70px; /* 减小Header高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 24px; /* 减小字体大小 */
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favicon-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favicon {
|
||||||
|
height: 45px; /* 减小图标大小 */
|
||||||
|
width: auto;
|
||||||
|
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主体内容样式优化 */
|
||||||
|
.login-main {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0; /* 允许flex收缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧登录表单容器优化 */
|
||||||
|
.login-form-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
width: 800px; /* 修复宽度 */
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30px; /* 减小内边距 */
|
||||||
|
overflow-y: auto; /* 允许内部滚动 */
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px; /* 减小最大宽度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0.75) 100%);
|
||||||
|
border-radius: 16px; /* 减小圆角 */
|
||||||
|
padding: 35px 30px; /* 减小内边距 */
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.12);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #1890ff 0%, #40a9ff 50%, #69c0ff 100%);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px; /* 减小间距 */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2 {
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 24px; /* 减小字体 */
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, #1890ff 0%, #40a9ff 100%);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式优化 */
|
||||||
|
.login-form {
|
||||||
|
margin-bottom: 25px; /* 减小间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
margin-bottom: 20px; /* 减小间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1.5px solid rgba(24, 144, 255, 0.15);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px; /* 减小内边距 */
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input :deep(.el-input__wrapper:hover) {
|
||||||
|
border-color: rgba(24, 144, 255, 0.4);
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input :deep(.el-input__wrapper.is-focus) {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.25);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px; /* 减小按钮高度 */
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #69c0ff 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
box-shadow: 0 6px 20px rgba(24, 144, 255, 0.25);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.25), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(24, 144, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 免责说明样式优化 */
|
||||||
|
.disclaimer {
|
||||||
|
margin-top: 25px; /* 减小间距 */
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid rgba(24, 144, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer-text {
|
||||||
|
font-size: 11px; /* 减小字体 */
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(249, 249, 249, 0.85) 0%, rgba(240, 248, 255, 0.85) 100%);
|
||||||
|
padding: 15px; /* 减小内边距 */
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 3px solid #1890ff;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计优化 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.login-form-container {
|
||||||
|
width: 380px;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-form-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 15px;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
padding: 12px 20px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favicon {
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-form-container {
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
padding: 25px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
padding: 8px 15px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favicon {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果优化 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-slide {
|
||||||
|
animation: slideZoom 25s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideZoom {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.login-form-container::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-container::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-container::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(24, 144, 255, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(24, 144, 255, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const { defineConfig } = require('@vue/cli-service')
|
||||||
|
module.exports = defineConfig({
|
||||||
|
transpileDependencies: true,
|
||||||
|
lintOnSave:false
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user