feat: init fronted

This commit is contained in:
2025-09-19 00:02:19 +08:00
parent 64039a90f3
commit 34b0e69ccc
17 changed files with 2309 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
+49
View File
@@ -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"
]
}
+28
View File
@@ -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

+17
View File
@@ -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>
+3
View File
@@ -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>
+58
View File
@@ -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>
+17
View File
@@ -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')
+42
View File
@@ -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
+145
View File
@@ -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
+249
View File
@@ -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>
+232
View File
@@ -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>
+382
View File
@@ -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>
+345
View File
@@ -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>
+586
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave:false
})