feat: add owner field, fix header issue and use zhCN for element-plus

This commit is contained in:
2025-09-19 09:31:45 +08:00
parent 6a5f074c1d
commit e3fa701f9f
8 changed files with 319 additions and 202 deletions
+4
View File
@@ -9,6 +9,9 @@ class Item(models.Model):
('in_use', '使用中'), ('in_use', '使用中'),
('maintenance', '维护中'), ('maintenance', '维护中'),
('damaged', '损坏'), ('damaged', '损坏'),
('lost', '丢失'),
('abandoned', '已弃用'),
('prohibited', '禁止借用'),
] ]
name = models.CharField(max_length=100, verbose_name='物品名称') name = models.CharField(max_length=100, verbose_name='物品名称')
@@ -17,6 +20,7 @@ class Item(models.Model):
category = models.CharField(max_length=50, verbose_name='物品类别') category = models.CharField(max_length=50, verbose_name='物品类别')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='available', verbose_name='物品状态') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='available', verbose_name='物品状态')
location = models.CharField(max_length=100, blank=True, verbose_name='存放位置') location = models.CharField(max_length=100, blank=True, verbose_name='存放位置')
owner = models.CharField(max_length=100, blank=True, null=True, verbose_name='所有者')
purchase_date = models.DateField(null=True, blank=True, verbose_name='购买日期') purchase_date = models.DateField(null=True, blank=True, verbose_name='购买日期')
value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='价值') value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='价值')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+1 -1
View File
@@ -22,7 +22,7 @@ class ItemSerializer(serializers.ModelSerializer):
model = Item model = Item
fields = [ fields = [
'id', 'name', 'description', 'serial_number', 'category', 'status', 'id', 'name', 'description', 'serial_number', 'category', 'status',
'location', 'purchase_date', 'value', 'created_at', 'updated_at', 'location', 'owner', 'purchase_date', 'value', 'created_at', 'updated_at',
'current_user' 'current_user'
] ]
+72
View File
@@ -0,0 +1,72 @@
<template>
<el-header class="app-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>
</template>
<script>
export default {
name: 'AppHeader'
}
</script>
<style scoped>
.app-header {
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 0 6px rgba(0,0,0,.04);
height: 60px !important;
display: flex;
align-items: center;
padding: 0 20px;
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.logo {
color: #409eff;
font-size: 20px;
font-weight: bold;
margin: 0;
}
.nav-menu {
border-bottom: none;
width: 60%;
background-color: transparent;
}
.nav-menu .el-menu-item {
border-bottom: none;
height: 60px;
line-height: 60px;
}
</style>
+20 -1
View File
@@ -4,6 +4,23 @@ import router from './router'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const debounce = (fn, delay) => {
let timeoutId
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn.apply(null, args), delay)
}
}
const _ResizeObserver = window.ResizeObserver
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
constructor(callback) {
callback = debounce(callback, 20)
super(callback)
}
}
const app = createApp(App) const app = createApp(App)
@@ -12,6 +29,8 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) app.component(key, component)
} }
app.use(ElementPlus) app.use(ElementPlus, {
locale: zhCn,
})
app.use(router) app.use(router)
app.mount('#app') app.mount('#app')
+91 -65
View File
@@ -1,30 +1,7 @@
<template> <template>
<el-header> <AppHeader />
<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"> <div class="dashboard">
<el-row :gutter="20"> <el-row :gutter="42">
<el-col :span="6"> <el-col :span="6">
<el-card class="stat-card"> <el-card class="stat-card">
<div class="stat-content"> <div class="stat-content">
@@ -77,6 +54,58 @@
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon damaged">
<el-icon><WarningFilled /></el-icon>
</div>
<div class="stat-info">
<h3>{{ stats.damaged }}</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 lost">
<el-icon><QuestionFilled /></el-icon>
</div>
<div class="stat-info">
<h3>{{ stats.lost }}</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 abandoned">
<el-icon><Delete /></el-icon>
</div>
<div class="stat-info">
<h3>{{ stats.abandoned }}</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 prohibited">
<el-icon><Lock /></el-icon>
</div>
<div class="stat-info">
<h3>{{ stats.prohibited }}</h3>
<p>禁止借用</p>
</div>
</div>
</el-card>
</el-col>
</el-row> </el-row>
<el-row :gutter="20" style="margin-top: 20px;"> <el-row :gutter="20" style="margin-top: 20px;">
@@ -127,16 +156,24 @@
<script> <script>
import { itemService, usageService } from '../services/api' import { itemService, usageService } from '../services/api'
import moment from 'moment' import moment from 'moment'
import AppHeader from '../components/AppHeader.vue'
export default { export default {
name: 'Dashboard', name: 'Dashboard',
components: {
AppHeader
},
data() { data() {
return { return {
stats: { stats: {
total: 0, total: 0,
available: 0, available: 0,
inUse: 0, inUse: 0,
maintenance: 0 maintenance: 0,
damaged: 0,
lost: 0,
abandoned: 0,
prohibited: 0,
}, },
currentUsages: [], currentUsages: [],
recentItems: [] recentItems: []
@@ -156,6 +193,10 @@ export default {
this.stats.available = items.filter(item => item.status === 'available').length this.stats.available = items.filter(item => item.status === 'available').length
this.stats.inUse = items.filter(item => item.status === 'in_use').length this.stats.inUse = items.filter(item => item.status === 'in_use').length
this.stats.maintenance = items.filter(item => item.status === 'maintenance').length this.stats.maintenance = items.filter(item => item.status === 'maintenance').length
this.stats.damaged = items.filter(item => item.status === 'damaged').length
this.stats.lost = items.filter(item => item.status === 'lost').length
this.stats.abandoned = items.filter(item => item.status === 'abandoned').length
this.stats.prohibited = items.filter(item => item.status === 'prohibited').length
// 获取最新物品 // 获取最新物品
this.recentItems = items.slice(0, 5) this.recentItems = items.slice(0, 5)
@@ -177,7 +218,10 @@ export default {
'available': 'success', 'available': 'success',
'in_use': 'warning', 'in_use': 'warning',
'maintenance': 'info', 'maintenance': 'info',
'damaged': 'danger' 'damaged': 'danger',
'lost': 'danger',
'abandoned': 'info',
'prohibited': 'warning',
} }
return typeMap[status] || 'info' return typeMap[status] || 'info'
}, },
@@ -186,7 +230,10 @@ export default {
'available': '可用', 'available': '可用',
'in_use': '使用中', 'in_use': '使用中',
'maintenance': '维护中', 'maintenance': '维护中',
'damaged': '损坏' 'damaged': '损坏',
'lost': '丢失',
'abandoned': '已弃用',
'prohibited': '禁止借用',
} }
return textMap[status] || '未知' return textMap[status] || '未知'
} }
@@ -195,43 +242,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.el-header {
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 0 6px rgba(0,0,0,.04);
height: 60px !important;
display: flex;
align-items: center;
padding: 0 20px;
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.logo {
color: #409eff;
font-size: 20px;
font-weight: bold;
margin: 0;
}
.nav-menu {
border-bottom: none;
background-color: transparent;
}
.nav-menu .el-menu-item {
border-bottom: none;
height: 60px;
line-height: 60px;
}
.dashboard { .dashboard {
padding: 20px; padding: 20px;
} }
@@ -273,6 +283,22 @@ export default {
background-color: #909399; background-color: #909399;
} }
.stat-icon.damaged {
background-color: #f56c6c;
}
.stat-icon.lost {
background-color: #f59e0b;
}
.stat-icon.abandoned {
background-color: #8e8e8e;
}
.stat-icon.prohibited {
background-color: #409eff;
}
.stat-info h3 { .stat-info h3 {
margin: 0; margin: 0;
font-size: 28px; font-size: 28px;
+11 -43
View File
@@ -1,28 +1,5 @@
<template> <template>
<el-header> <AppHeader />
<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="item-detail">
<div class="detail-header"> <div class="detail-header">
<el-button @click="$router.go(-1)" style="margin-bottom: 20px;"> <el-button @click="$router.go(-1)" style="margin-bottom: 20px;">
@@ -60,11 +37,17 @@
<el-option label="使用中" value="in_use" /> <el-option label="使用中" value="in_use" />
<el-option label="维护中" value="maintenance" /> <el-option label="维护中" value="maintenance" />
<el-option label="损坏" value="damaged" /> <el-option label="损坏" value="damaged" />
<el-option label="丢失" value="lost" />
<el-option label="已弃用" value="abandoned" />
<el-option label="禁止借用" value="prohibited" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="位置"> <el-form-item label="位置">
<el-input v-model="editableItem.location" /> <el-input v-model="editableItem.location" />
</el-form-item> </el-form-item>
<el-form-item label="所有者">
<el-input v-model="editableItem.owner" :disabled="!editMode" placeholder="请输入所有者姓名" />
</el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="购买日期"> <el-form-item label="购买日期">
@@ -160,10 +143,14 @@
<script> <script>
import { itemService } from '../services/api' import { itemService } from '../services/api'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import AppHeader from '../components/AppHeader.vue'
import moment from 'moment' import moment from 'moment'
export default { export default {
name: 'ItemDetail', name: 'ItemDetail',
components: {
AppHeader
},
props: { props: {
id: { id: {
type: String, type: String,
@@ -220,25 +207,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.el-header {
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 0 6px rgba(0,0,0,.04);
height: 60px !important;
display: flex;
align-items: center;
padding: 0 20px;
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.item-detail { .item-detail {
padding: 20px; padding: 20px;
} }
+115 -49
View File
@@ -1,28 +1,5 @@
<template> <template>
<el-header> <AppHeader />
<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="item-list">
<div class="toolbar"> <div class="toolbar">
<el-button type="primary" @click="showAddDialog = true"> <el-button type="primary" @click="showAddDialog = true">
@@ -46,6 +23,9 @@
<el-option label="使用中" value="in_use" /> <el-option label="使用中" value="in_use" />
<el-option label="维护中" value="maintenance" /> <el-option label="维护中" value="maintenance" />
<el-option label="损坏" value="damaged" /> <el-option label="损坏" value="damaged" />
<el-option label="丢失" value="lost" />
<el-option label="已弃用" value="abandoned" />
<el-option label="禁止借用" value="prohibited" />
</el-select> </el-select>
</div> </div>
</div> </div>
@@ -61,6 +41,14 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="owner" label="所有者">
<template #default="scope">
<span v-if="scope.row.owner">
{{ scope.row.owner }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="current_user" label="当前使用者"> <el-table-column prop="current_user" label="当前使用者">
<template #default="scope"> <template #default="scope">
<span v-if="scope.row.current_user"> <span v-if="scope.row.current_user">
@@ -122,6 +110,9 @@
<el-form-item label="购买日期"> <el-form-item label="购买日期">
<el-date-picker v-model="newItem.purchase_date" type="date" /> <el-date-picker v-model="newItem.purchase_date" type="date" />
</el-form-item> </el-form-item>
<el-form-item label="所有者">
<el-input v-model="newItem.owner" placeholder="请输入所有者姓名" />
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="showAddDialog = false">取消</el-button> <el-button @click="showAddDialog = false">取消</el-button>
@@ -173,15 +164,64 @@
<el-button type="primary" @click="confirmReturn">确认归还</el-button> <el-button type="primary" @click="confirmReturn">确认归还</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 编辑物品对话框 -->
<el-dialog v-model="showEditDialog" title="编辑物品" width="600px">
<el-form :model="editForm" label-width="100px" :rules="itemRules" ref="editForm">
<el-form-item label="物品名称" prop="name">
<el-input v-model="editForm.name" />
</el-form-item>
<el-form-item label="序列号" prop="serial_number">
<el-input v-model="editForm.serial_number" />
</el-form-item>
<el-form-item label="类别" prop="category">
<el-input v-model="editForm.category" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="editForm.status">
<el-option label="可用" value="available" />
<el-option label="使用中" value="in_use" />
<el-option label="维护中" value="maintenance" />
<el-option label="损坏" value="damaged" />
<el-option label="丢失" value="lost" />
<el-option label="已弃用" value="abandoned" />
<el-option label="禁止借用" value="prohibited" />
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input type="textarea" v-model="editForm.description" />
</el-form-item>
<el-form-item label="位置">
<el-input v-model="editForm.location" />
</el-form-item>
<el-form-item label="价值">
<el-input-number v-model="editForm.value" :precision="2" :min="0" />
</el-form-item>
<el-form-item label="购买日期">
<el-date-picker v-model="editForm.purchase_date" type="date" />
</el-form-item>
<el-form-item label="所有者" prop="owner_id">
<el-input v-model="editForm.owner" placeholder="请输入所有者姓名" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditDialog = false">取消</el-button>
<el-button type="primary" @click="updateItem">保存修改</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { itemService, userService } from '../services/api' import { itemService, userService } from '../services/api'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import AppHeader from '../components/AppHeader.vue'
export default { export default {
name: 'ItemList', name: 'ItemList',
components: {
AppHeader
},
data() { data() {
return { return {
items: [], items: [],
@@ -192,6 +232,7 @@ export default {
showAddDialog: false, showAddDialog: false,
showBorrowDialog: false, showBorrowDialog: false,
showReturnDialog: false, showReturnDialog: false,
showEditDialog: false,
currentItem: null, currentItem: null,
newItem: { newItem: {
name: '', name: '',
@@ -200,7 +241,8 @@ export default {
description: '', description: '',
location: '', location: '',
value: null, value: null,
purchase_date: null purchase_date: null,
owner: ''
}, },
borrowForm: { borrowForm: {
user_id: null, user_id: null,
@@ -212,6 +254,18 @@ export default {
condition_after: '', condition_after: '',
return_notes: '' return_notes: ''
}, },
editForm: {
id: null,
name: '',
serial_number: '',
category: '',
status: '',
description: '',
location: '',
value: null,
purchase_date: null,
owner: ''
},
itemRules: { itemRules: {
name: [{ required: true, message: '请输入物品名称', trigger: 'blur' }], name: [{ required: true, message: '请输入物品名称', trigger: 'blur' }],
serial_number: [{ required: true, message: '请输入序列号', trigger: 'blur' }], serial_number: [{ required: true, message: '请输入序列号', trigger: 'blur' }],
@@ -272,8 +326,9 @@ export default {
this.$router.push(`/items/${id}`) this.$router.push(`/items/${id}`)
}, },
editItem(item) { editItem(item) {
// TODO: 实现编辑功能 this.currentItem = item
ElMessage.info('编辑功能开发中') this.editForm = { ...item }
this.showEditDialog = true
}, },
borrowItem(item) { borrowItem(item) {
this.currentItem = item this.currentItem = item
@@ -317,7 +372,8 @@ export default {
description: '', description: '',
location: '', location: '',
value: null, value: null,
purchase_date: null purchase_date: null,
owner: ''
} }
await this.loadItems() await this.loadItems()
} catch (error) { } catch (error) {
@@ -352,12 +408,38 @@ export default {
ElMessage.error('归还失败') ElMessage.error('归还失败')
} }
}, },
async updateItem() {
try {
await this.$refs.editForm.validate()
const itemData = { ...this.editForm }
// 格式化购买日期
if (itemData.purchase_date) {
const date = new Date(itemData.purchase_date)
itemData.purchase_date = date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0')
}
await itemService.updateItem(itemData.id, itemData)
ElMessage.success('物品信息更新成功')
this.showEditDialog = false
await this.loadItems()
} catch (error) {
console.error('更新物品失败:', error)
ElMessage.error('更新物品失败')
}
},
getStatusType(status) { getStatusType(status) {
const typeMap = { const typeMap = {
'available': 'success', 'available': 'success',
'in_use': 'warning', 'in_use': 'warning',
'maintenance': 'info', 'maintenance': 'info',
'damaged': 'danger' 'damaged': 'danger',
'lost': 'danger',
'abandoned': 'info',
'prohibited': 'warning'
} }
return typeMap[status] || 'info' return typeMap[status] || 'info'
}, },
@@ -366,7 +448,10 @@ export default {
'available': '可用', 'available': '可用',
'in_use': '使用中', 'in_use': '使用中',
'maintenance': '维护中', 'maintenance': '维护中',
'damaged': '损坏' 'damaged': '损坏',
'lost': '丢失',
'abandoned': '已弃用',
'prohibited': '禁止借用'
} }
return textMap[status] || '未知' return textMap[status] || '未知'
} }
@@ -375,25 +460,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.el-header {
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 0 6px rgba(0,0,0,.04);
height: 60px !important;
display: flex;
align-items: center;
padding: 0 20px;
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.item-list { .item-list {
padding: 20px; padding: 20px;
} }
+5 -43
View File
@@ -1,28 +1,5 @@
<template> <template>
<el-header> <AppHeader />
<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="item-usage">
<div class="toolbar"> <div class="toolbar">
<h2>使用记录</h2> <h2>使用记录</h2>
@@ -176,9 +153,13 @@
import { usageService, userService } from '../services/api' import { usageService, userService } from '../services/api'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import moment from 'moment' import moment from 'moment'
import AppHeader from '../components/AppHeader.vue'
export default { export default {
name: 'ItemUsage', name: 'ItemUsage',
components: {
AppHeader
},
data() { data() {
return { return {
usages: [], usages: [],
@@ -312,25 +293,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.el-header {
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 0 6px rgba(0,0,0,.04);
height: 60px !important;
display: flex;
align-items: center;
padding: 0 20px;
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.item-usage { .item-usage {
padding: 20px; padding: 20px;
} }