Compare commits
2 Commits
3c838d534f
...
70d0dfc427
| Author | SHA1 | Date | |
|---|---|---|---|
|
70d0dfc427
|
|||
|
e24a34deb4
|
38
desktop-ui/.gitignore
vendored
Normal file
38
desktop-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Vitest
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
*.timestamp-*-*.mjs
|
||||||
22
desktop-ui/index.html
Normal file
22
desktop-ui/index.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vite App</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
8
desktop-ui/jsconfig.json
Normal file
8
desktop-ui/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
3020
desktop-ui/package-lock.json
generated
Normal file
3020
desktop-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
desktop-ui/package.json
Normal file
25
desktop-ui/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "desktop-ui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.16.0",
|
||||||
|
"element-plus": "^2.13.7",
|
||||||
|
"vue": "^3.5.32",
|
||||||
|
"vue-router": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
|
"vite": "^8.0.8",
|
||||||
|
"vite-plugin-vue-devtools": "^8.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
desktop-ui/public/favicon.ico
Normal file
BIN
desktop-ui/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
346
desktop-ui/src/App.vue
Normal file
346
desktop-ui/src/App.vue
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
import { Setting, SwitchButton, Odometer, Collection, Timer, InfoFilled } from '@element-plus/icons-vue'
|
||||||
|
import { ref, computed, getCurrentInstance } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import Login from './views/Login.vue'
|
||||||
|
|
||||||
|
const { $http } = getCurrentInstance().appContext.config.globalProperties
|
||||||
|
|
||||||
|
const nickname = ref(localStorage.getItem('nickname'))
|
||||||
|
const role = ref(localStorage.getItem('role'))
|
||||||
|
const isAdmin = computed(() => role.value === 'admin')
|
||||||
|
|
||||||
|
// 用户管理
|
||||||
|
const userDialogVisible = ref(false)
|
||||||
|
const users = ref([])
|
||||||
|
const userForm = ref({ username: '', nickname: '', password: '' })
|
||||||
|
const userFormVisible = ref(false)
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userid')
|
||||||
|
localStorage.removeItem('nickname')
|
||||||
|
localStorage.removeItem('role')
|
||||||
|
role.value = null
|
||||||
|
nickname.value = ''
|
||||||
|
ElMessage.success('已退出登录')
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openUserManager() {
|
||||||
|
await loadUsers()
|
||||||
|
userDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
|
const res = await $http.get('/user')
|
||||||
|
users.value = res.data || []
|
||||||
|
} catch {
|
||||||
|
users.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateUser() {
|
||||||
|
userForm.value = { username: '', nickname: '', password: '' }
|
||||||
|
userFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUser() {
|
||||||
|
if (!userForm.value.username || !userForm.value.password) {
|
||||||
|
ElMessage.error('用户名和密码不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await $http.post('/user', userForm.value)
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
userFormVisible.value = false
|
||||||
|
await loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || '创建失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除用户"${row.username}"吗?`, '确认删除', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
await $http.delete(`/user/${row._id}`)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await loadUsers()
|
||||||
|
} catch {
|
||||||
|
// cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 nickname/role(Login.vue 登录成功后调用)
|
||||||
|
window.__updateUserInfo = (info) => {
|
||||||
|
nickname.value = info.nickname
|
||||||
|
role.value = info.role
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-container class="layout-container" style="height: 100vh">
|
||||||
|
<el-aside width="200px" class="side-bar">
|
||||||
|
<div class="logo-area">
|
||||||
|
<span class="logo-icon">🔬</span>
|
||||||
|
<span class="logo-text">LabManager</span>
|
||||||
|
</div>
|
||||||
|
<el-menu :default-active="$route.path" router class="side-menu" :collapse-transition="false">
|
||||||
|
<el-menu-item index="/">
|
||||||
|
<el-icon>
|
||||||
|
<Odometer />
|
||||||
|
</el-icon>
|
||||||
|
<span>看板</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/standard">
|
||||||
|
<el-icon>
|
||||||
|
<Collection />
|
||||||
|
</el-icon>
|
||||||
|
<span>对照品</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/stability">
|
||||||
|
<el-icon>
|
||||||
|
<Timer />
|
||||||
|
</el-icon>
|
||||||
|
<span>稳定性</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/about">
|
||||||
|
<el-icon>
|
||||||
|
<InfoFilled />
|
||||||
|
</el-icon>
|
||||||
|
<span>关于</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-container>
|
||||||
|
<el-header class="top-bar">
|
||||||
|
<div class="header-left">
|
||||||
|
<span class="page-label">{{ $route.meta?.title || '' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="32" class="user-avatar">{{ nickname?.charAt(0) || '?' }}</el-avatar>
|
||||||
|
<span class="user-name">{{ nickname }}</span>
|
||||||
|
</div>
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-if="isAdmin" @click="openUserManager">
|
||||||
|
<el-icon :size="16">
|
||||||
|
<Setting />
|
||||||
|
</el-icon>
|
||||||
|
用户管理
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided @click="logout">
|
||||||
|
<el-icon :size="16">
|
||||||
|
<SwitchButton />
|
||||||
|
</el-icon>
|
||||||
|
退出登录
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
<el-button class="setting-btn" circle :icon="Setting" size="small" />
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<el-main class="main-area">
|
||||||
|
<RouterView />
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
|
||||||
|
<Login />
|
||||||
|
|
||||||
|
<!-- 用户管理对话框 -->
|
||||||
|
<el-dialog v-model="userDialogVisible" title="用户管理" width="600px" align-center>
|
||||||
|
<div class="user-mgr-toolbar">
|
||||||
|
<span class="user-count">共 {{ users.length }} 个用户</span>
|
||||||
|
<el-button type="primary" size="small" @click="openCreateUser">新增用户</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="users" stripe size="small">
|
||||||
|
<el-table-column prop="username" label="用户名" />
|
||||||
|
<el-table-column prop="nickname" label="昵称" />
|
||||||
|
<el-table-column prop="role" label="角色" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'" size="small">
|
||||||
|
{{ row.role === 'admin' ? '管理员' : '用户' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="danger" link @click="deleteUser(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 新增用户对话框 -->
|
||||||
|
<el-dialog v-model="userFormVisible" title="新增用户" width="420px" align-center>
|
||||||
|
<el-form :model="userForm" label-position="top">
|
||||||
|
<el-form-item label="用户名" required>
|
||||||
|
<el-input v-model="userForm.username" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称">
|
||||||
|
<el-input v-model="userForm.nickname" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" required>
|
||||||
|
<el-input v-model="userForm.password" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="userFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="createUser">创建</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
* {
|
||||||
|
font-family:
|
||||||
|
Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 整体布局 ===== */
|
||||||
|
.layout-container {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 侧边栏 ===== */
|
||||||
|
.side-bar {
|
||||||
|
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 22px 20px 18px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e0e6f0;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu {
|
||||||
|
flex: 1;
|
||||||
|
border-right: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu .el-menu-item {
|
||||||
|
color: rgba(224, 230, 240, 0.65);
|
||||||
|
font-size: 14px;
|
||||||
|
height: 46px;
|
||||||
|
line-height: 46px;
|
||||||
|
margin: 2px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu .el-menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #e0e6f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu .el-menu-item.is-active {
|
||||||
|
background: linear-gradient(135deg, #4f8cff 0%, #6c5ce7 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 4px 12px rgba(79, 140, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu .el-menu-item .el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 顶栏 ===== */
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 56px !important;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e8ecf1;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||||
|
padding: 0 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-label {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
background: linear-gradient(135deg, #4f8cff, #6c5ce7);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f2a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-btn {
|
||||||
|
--el-bg-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8896a8;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-btn:hover {
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #4f8cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 主内容区 ===== */
|
||||||
|
.main-area {
|
||||||
|
padding: 0 !important;
|
||||||
|
background: #f5f7fa;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
desktop-ui/src/main.js
Normal file
31
desktop-ui/src/main.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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 zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus, { locale: zhCn })
|
||||||
|
|
||||||
|
const http = axios.create({
|
||||||
|
baseURL: 'https://solidaim.cn/api',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
http.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.set('Authorization', `Bearer ${token}`)
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
app.config.globalProperties.$http = http
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
37
desktop-ui/src/router/index.js
Normal file
37
desktop-ui/src/router/index.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import HomeView from "../views/HomeView.vue";
|
||||||
|
import Standard from "../views/Standard.vue";
|
||||||
|
import Stability from "../views/Stability.vue";
|
||||||
|
import AboutView from "../views/AboutView.vue";
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
meta: { title: "仪表板" },
|
||||||
|
component: HomeView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/standard",
|
||||||
|
name: "standard",
|
||||||
|
meta: { title: "对照品管理" },
|
||||||
|
component: Standard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/stability",
|
||||||
|
name: "stability",
|
||||||
|
meta: { title: "稳定性实验管理" },
|
||||||
|
component: Stability,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/about",
|
||||||
|
name: "about",
|
||||||
|
meta: { title: "关于" },
|
||||||
|
component: AboutView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
180
desktop-ui/src/views/AboutView.vue
Normal file
180
desktop-ui/src/views/AboutView.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about-page">
|
||||||
|
<div class="hero">
|
||||||
|
<div class="hero-icon">🔬</div>
|
||||||
|
<h1 class="hero-title">LabManager</h1>
|
||||||
|
<p class="hero-desc">实验室对照品与稳定性实验管理系统</p>
|
||||||
|
<div class="hero-badges">
|
||||||
|
<el-tag>v1.0.0</el-tag>
|
||||||
|
<el-tag type="success">MIT License</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="24" class="content-row">
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-card shadow="never" class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<span class="card-title">📖 项目介绍</span>
|
||||||
|
</template>
|
||||||
|
<p>LabManager 是一款面向药品与化工实验室的轻量级管理工具,旨在帮助实验室高效管理对照品(标准物质)和稳定性实验的全流程。</p>
|
||||||
|
<p>系统提供了对照品的入库、存储位置追踪、有效期预警管理,以及稳定性实验的批次规划、检查点跟踪、样品取出确认等功能,让实验室数据管理更加规范、可追溯。</p>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<span class="card-title">⚙️ 技术栈</span>
|
||||||
|
</template>
|
||||||
|
<div class="tech-stack">
|
||||||
|
<div class="tech-group">
|
||||||
|
<h4>前端</h4>
|
||||||
|
<div class="tech-tags">
|
||||||
|
<el-tag>Vue 3</el-tag>
|
||||||
|
<el-tag>Vite</el-tag>
|
||||||
|
<el-tag>Element Plus</el-tag>
|
||||||
|
<el-tag>Axios</el-tag>
|
||||||
|
<el-tag>Vue Router</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tech-group">
|
||||||
|
<h4>后端</h4>
|
||||||
|
<div class="tech-tags">
|
||||||
|
<el-tag>Node.js</el-tag>
|
||||||
|
<el-tag>Express</el-tag>
|
||||||
|
<el-tag>MongoDB</el-tag>
|
||||||
|
<el-tag>Mongoose</el-tag>
|
||||||
|
<el-tag>JWT</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="never" class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<span class="card-title">📋 功能列表</span>
|
||||||
|
</template>
|
||||||
|
<el-timeline>
|
||||||
|
<el-timeline-item timestamp="看板" placement="top">
|
||||||
|
数据概览与快捷入口
|
||||||
|
</el-timeline-item>
|
||||||
|
<el-timeline-item timestamp="对照品管理" placement="top">
|
||||||
|
批号 / 含量 / 纯度 / 有效期 / 存放位置 CRUD
|
||||||
|
</el-timeline-item>
|
||||||
|
<el-timeline-item timestamp="稳定性实验" placement="top">
|
||||||
|
批次规划 / 检查点跟踪 / 样品取出确认
|
||||||
|
</el-timeline-item>
|
||||||
|
<el-timeline-item timestamp="用户管理" placement="top">
|
||||||
|
管理员用户创建与权限控制
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="info-card">
|
||||||
|
<template #header>
|
||||||
|
<span class="card-title">👤 关于作者</span>
|
||||||
|
</template>
|
||||||
|
<p class="author-line">本项目由实验室管理团队开发和维护。</p>
|
||||||
|
<p class="author-line">如有问题或建议,欢迎联系管理员。</p>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.about-page {
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Hero 区域 ===== */
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-icon {
|
||||||
|
font-size: 56px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2a3a;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #8896a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 内容卡片 ===== */
|
||||||
|
.content-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e8ecf1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card :deep(.el-card__header) {
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #3d4a5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 技术栈 ===== */
|
||||||
|
.tech-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-group h4 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8896a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 作者 ===== */
|
||||||
|
.author-line {
|
||||||
|
margin: 0 0 4px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
191
desktop-ui/src/views/HomeView.vue
Normal file
191
desktop-ui/src/views/HomeView.vue
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const { $http } = getCurrentInstance().appContext.config.globalProperties
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const standardCount = ref(0)
|
||||||
|
const stabilityCount = ref(0)
|
||||||
|
const pendingChecks = ref(0)
|
||||||
|
const latestStandards = ref([])
|
||||||
|
const latestStabilities = ref([])
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!localStorage.getItem('token')) return
|
||||||
|
try {
|
||||||
|
const [stdRes, stabRes] = await Promise.all([$http.get('/standard'), $http.get('/stability')])
|
||||||
|
const standards = stdRes.data || []
|
||||||
|
const stabilities = stabRes.data || []
|
||||||
|
standardCount.value = standards.length
|
||||||
|
stabilityCount.value = stabilities.length
|
||||||
|
pendingChecks.value = count(stabilities)
|
||||||
|
|
||||||
|
latestStandards.value = standards.slice(-5).reverse()
|
||||||
|
latestStabilities.value = stabilities.slice(-5).reverse()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function count(stabilities) {
|
||||||
|
let count = 0
|
||||||
|
for (const item of stabilities) {
|
||||||
|
if (item.checks && Array.isArray(item.checks)) {
|
||||||
|
for (const check of item.checks) {
|
||||||
|
if (check.checked) continue
|
||||||
|
const checkDate = new Date(check.date)
|
||||||
|
if (subDate(checkDate, new Date()) < 30) {
|
||||||
|
count++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算 d1 和 d2 之间的天数
|
||||||
|
function subDate(d1, d2) {
|
||||||
|
const diff = Math.abs(d1 - d2)
|
||||||
|
const day = 24 * 60 * 60 * 1000
|
||||||
|
return Math.ceil(diff / day)
|
||||||
|
}
|
||||||
|
|
||||||
|
function go(path) {
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<el-row :gutter="20" class="stat-cards">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover" class="stat-card" @click="go('/standard')">
|
||||||
|
<div class="stat-inner">
|
||||||
|
<div class="stat-value">{{ standardCount }}</div>
|
||||||
|
<div class="stat-label">对照品总数</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover" class="stat-card" @click="go('/stability')">
|
||||||
|
<div class="stat-inner">
|
||||||
|
<div class="stat-value">{{ stabilityCount }}</div>
|
||||||
|
<div class="stat-label">稳定性实验总数</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover" class="stat-card warning" @click="go('/stability')">
|
||||||
|
<div class="stat-inner">
|
||||||
|
<div class="stat-value">{{ pendingChecks }}</div>
|
||||||
|
<div class="stat-label">稳定性实验待取出项</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" class="recent-lists">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>最近对照品</span>
|
||||||
|
<el-button text type="primary" @click="go('/standard')">查看全部</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="latestStandards" size="small" stripe>
|
||||||
|
<el-table-column prop="batch" label="批号" />
|
||||||
|
<el-table-column prop="location" label="位置" />
|
||||||
|
<el-table-column prop="calibration_date" label="标定日期">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.calibration_date ? new Date(row.calibration_date).toLocaleDateString() : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="latestStandards.length === 0" description="暂无数据" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>最近稳定性实验</span>
|
||||||
|
<el-button text type="primary" @click="go('/stability')">查看全部</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="latestStabilities" size="small" stripe>
|
||||||
|
<el-table-column prop="batch" label="批次" />
|
||||||
|
<el-table-column prop="type" label="类型" width="80" />
|
||||||
|
<el-table-column label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.ended ? 'success' : 'warning'" size="small">
|
||||||
|
{{ row.ended ? '已完成' : '进行中' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="latestStabilities.length === 0" description="暂无数据" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-cards {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-inner {
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.warning .stat-value {
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-lists {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header span {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
desktop-ui/src/views/Login.vue
Normal file
61
desktop-ui/src/views/Login.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { computed, getCurrentInstance, ref } from 'vue'
|
||||||
|
|
||||||
|
const { $http } = getCurrentInstance().appContext.config.globalProperties
|
||||||
|
|
||||||
|
const pleaseLogin = ref(localStorage.getItem('token') == null)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
username: "",
|
||||||
|
password: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
try {
|
||||||
|
if (!form.value.username || !form.value.password) {
|
||||||
|
ElMessage.error("请输入账号和密码!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await $http.post("/user/login", form.value)
|
||||||
|
const { token, user } = res.data
|
||||||
|
localStorage.setItem("token", token)
|
||||||
|
localStorage.setItem("userid", user._id)
|
||||||
|
localStorage.setItem("nickname", user.nickname)
|
||||||
|
localStorage.setItem("role", user.role)
|
||||||
|
if (window.__updateUserInfo) {
|
||||||
|
window.__updateUserInfo({ nickname: user.nickname, role: user.role })
|
||||||
|
}
|
||||||
|
ElMessage.success("登录成功!")
|
||||||
|
pleaseLogin.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog v-model="pleaseLogin" width="500px" title="请登录" align-center :show-close="false"
|
||||||
|
:close-on-click-modal="false" :close-on-press-escape="false">
|
||||||
|
<el-form :model="form" label-position="top">
|
||||||
|
<el-form-item label="用户名" required>
|
||||||
|
<el-input v-model="form.username"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密 码" required>
|
||||||
|
<el-input v-model="form.password" show-password></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="login()">登录</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family:
|
||||||
|
Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
506
desktop-ui/src/views/Stability.vue
Normal file
506
desktop-ui/src/views/Stability.vue
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const { $http } = getCurrentInstance().appContext.config.globalProperties
|
||||||
|
|
||||||
|
const isAdmin = localStorage.getItem('role') === 'admin'
|
||||||
|
const data = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editing = ref(false)
|
||||||
|
const form = ref({
|
||||||
|
batch: '',
|
||||||
|
type: '长期',
|
||||||
|
description: '',
|
||||||
|
checks: [],
|
||||||
|
})
|
||||||
|
const newCheckDate = ref('')
|
||||||
|
const newCheckDesc = ref('')
|
||||||
|
const search = ref('')
|
||||||
|
const expandAll = ref(false)
|
||||||
|
const selectedIds = ref([])
|
||||||
|
const expandRowKeys = ref([])
|
||||||
|
|
||||||
|
// 筛查天数(0 = 显示全部)
|
||||||
|
const filterDays = ref(0)
|
||||||
|
const filteredData = computed(() => {
|
||||||
|
const filted = search.value == '' ? data.value : data.value.filter((v) => v.batch.includes(search.value))
|
||||||
|
if (!filterDays.value || filterDays.value <= 0) return filted
|
||||||
|
const now = new Date()
|
||||||
|
const limit = new Date(now.getTime() + filterDays.value * 86400000)
|
||||||
|
return filted.filter((item) => {
|
||||||
|
if (item.ended) return false
|
||||||
|
return (item.checks || []).some((chk) => {
|
||||||
|
if (chk.checked) return false
|
||||||
|
const chkDate = new Date(chk.date)
|
||||||
|
return chkDate >= now && chkDate <= limit
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 根据实验类型生成默认检查点(日期从今天开始计算) */
|
||||||
|
function generateDefaultChecks(type) {
|
||||||
|
const today = new Date()
|
||||||
|
const monthsMap = {
|
||||||
|
加速: [0, 1, 2, 3, 6],
|
||||||
|
长期: [0, 3, 6, 12, 18, 24, 36, 48],
|
||||||
|
}
|
||||||
|
const months = monthsMap[type]
|
||||||
|
if (!months) return []
|
||||||
|
|
||||||
|
return months.map((m) => {
|
||||||
|
const date = new Date(today)
|
||||||
|
date.setMonth(date.getMonth() + m)
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const mo = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
return {
|
||||||
|
date: `${y}-${mo}-${d}`,
|
||||||
|
description: `${m}个月`,
|
||||||
|
checked: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
const res = await $http.get('/stability')
|
||||||
|
const list = res.data || []
|
||||||
|
// 未完成的排前面,已完成的排后面
|
||||||
|
list.sort((a, b) => {
|
||||||
|
if (a.ended === b.ended) return 0
|
||||||
|
return a.ended ? 1 : -1
|
||||||
|
})
|
||||||
|
data.value = list
|
||||||
|
// 只默认展开未完成的行
|
||||||
|
expandRowKeys.value = list.filter((r) => !r.ended).map((r) => r._id)
|
||||||
|
} catch {
|
||||||
|
data.value = []
|
||||||
|
expandRowKeys.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editing.value = false
|
||||||
|
form.value = { batch: '', type: '长期', description: '', checks: [] }
|
||||||
|
// 根据默认类型填充检查点
|
||||||
|
form.value.checks = generateDefaultChecks('长期')
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换实验类型时自动重新生成检查点(仅新建模式)
|
||||||
|
watch(
|
||||||
|
() => form.value.type,
|
||||||
|
(newType) => {
|
||||||
|
if (!editing.value) {
|
||||||
|
form.value.checks = generateDefaultChecks(newType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
editing.value = true
|
||||||
|
form.value = {
|
||||||
|
batch: row.batch,
|
||||||
|
type: row.type,
|
||||||
|
description: row.description || '',
|
||||||
|
checks: (row.checks || []).map((c) => ({ ...c })),
|
||||||
|
}
|
||||||
|
form.value._id = row._id
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCheck() {
|
||||||
|
if (!newCheckDate.value) {
|
||||||
|
ElMessage.error('请选择检查日期')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.checks.push({
|
||||||
|
date: newCheckDate.value,
|
||||||
|
description: newCheckDesc.value || '',
|
||||||
|
checked: false,
|
||||||
|
})
|
||||||
|
newCheckDate.value = ''
|
||||||
|
newCheckDesc.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCheck(index) {
|
||||||
|
form.value.checks.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!form.value.batch) {
|
||||||
|
ElMessage.error('批次号不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.value.checks.length === 0) {
|
||||||
|
ElMessage.error('请至少添加一个检查点')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (editing.value) {
|
||||||
|
await $http.patch(`/stability/${form.value._id}`, {
|
||||||
|
batch: form.value.batch,
|
||||||
|
type: form.value.type,
|
||||||
|
description: form.value.description,
|
||||||
|
checks: form.value.checks,
|
||||||
|
})
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await $http.post('/stability', {
|
||||||
|
batch: form.value.batch,
|
||||||
|
type: form.value.type,
|
||||||
|
description: form.value.description,
|
||||||
|
checks: form.value.checks,
|
||||||
|
})
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
await refresh()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除批次"${row.batch}"的实验记录吗?`, '确认删除', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
await $http.delete(`/stability/${row._id}`)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await refresh()
|
||||||
|
} catch {
|
||||||
|
// cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCheck(row) {
|
||||||
|
try {
|
||||||
|
const check = row.checks.find((v) => !v.checked)
|
||||||
|
await ElMessageBox.confirm(`确认取出批次 "${row.batch}" 的 "${check.description}" 样品?`, '确认取出', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'info',
|
||||||
|
})
|
||||||
|
await $http.post(`/stability/check/${row._id}/${check._id}`)
|
||||||
|
ElMessage.success('已确认取出')
|
||||||
|
await refresh()
|
||||||
|
} catch {
|
||||||
|
// cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandRow() {
|
||||||
|
expandRowKeys.value = []
|
||||||
|
expandAll.value = !expandAll.value
|
||||||
|
if (expandAll.value) filteredData.value.forEach((v) => expandRowKeys.value.push(v._id))
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStatus(value, row) {
|
||||||
|
return row.ended == value
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterType(value, row) {
|
||||||
|
return row.type == value
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkProgress(checks) {
|
||||||
|
if (!checks || checks.length === 0) return 0
|
||||||
|
const done = checks.filter((c) => c.checked).length
|
||||||
|
return Math.round((done / checks.length) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const d = new Date(date)
|
||||||
|
let year = d.getFullYear()
|
||||||
|
let month = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
let day = d.getDate().toString().padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(refresh)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="actions-bar">
|
||||||
|
<div class="actions-left">
|
||||||
|
<el-button @click="refresh">刷新</el-button>
|
||||||
|
<el-button type="primary" @click="openCreate">新增实验</el-button>
|
||||||
|
<el-input v-model="search" style="width: 240px" placeholder="输入进行搜索" clearable></el-input>
|
||||||
|
</div>
|
||||||
|
<div class="actions-right">
|
||||||
|
<span class="filter-label">筛查</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="filterDays"
|
||||||
|
:min="0"
|
||||||
|
:max="365"
|
||||||
|
:step="7"
|
||||||
|
size="small"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100px"
|
||||||
|
/>
|
||||||
|
<span class="filter-unit">天内需取出</span>
|
||||||
|
<el-tag v-if="filterDays > 0" type="warning" size="small" effect="plain" class="filter-hint">
|
||||||
|
显示 {{ filteredData.length }} 条
|
||||||
|
</el-tag>
|
||||||
|
<el-button v-if="filterDays > 0" size="small" text type="info" @click="filterDays = 0">
|
||||||
|
清除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="filteredData" stripe border style="width: 100%" row-key="_id" :expand-row-keys="expandRowKeys">
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #header>
|
||||||
|
<el-button
|
||||||
|
text
|
||||||
|
@click="expandRow"
|
||||||
|
:icon="expandAll ? ArrowDown : ArrowRight"
|
||||||
|
class="el-table__expand-icon"
|
||||||
|
></el-button>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="expand-content">
|
||||||
|
<el-steps :active="(row.checks || []).filter((c) => c.checked).length" align-center>
|
||||||
|
<el-step
|
||||||
|
v-for="chk in row.checks || []"
|
||||||
|
:key="chk._id || chk.date"
|
||||||
|
:title="chk.description || '-'"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<div class="step-desc">
|
||||||
|
<span>{{ formatDate(chk.date) }}</span>
|
||||||
|
<span v-if="chk.checked && chk.operatorName" class="step-op"
|
||||||
|
>👤{{ chk.operatorName }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #icon>
|
||||||
|
<el-tag
|
||||||
|
:type="chk.checked ? 'success' : 'info'"
|
||||||
|
size="small"
|
||||||
|
effect="plain"
|
||||||
|
style="border: none; padding: 0 2px"
|
||||||
|
>
|
||||||
|
{{ chk.checked ? '✓' : '○' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-step>
|
||||||
|
</el-steps>
|
||||||
|
<el-empty v-if="!(row.checks && row.checks.length)" description="暂无检查点" :image-size="60" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="batch" label="批次" min-width="160" />
|
||||||
|
<el-table-column
|
||||||
|
prop="type"
|
||||||
|
label="类型"
|
||||||
|
width="70"
|
||||||
|
:filters="[
|
||||||
|
{ text: '长期', value: '长期' },
|
||||||
|
{ text: '加速', value: '加速' },
|
||||||
|
{ text: '其它', value: '其它' },
|
||||||
|
]"
|
||||||
|
:filter-method="filterType"
|
||||||
|
/>
|
||||||
|
<el-table-column label="进度" width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-progress
|
||||||
|
:percentage="checkProgress(row.checks)"
|
||||||
|
:status="row.ended ? 'success' : undefined"
|
||||||
|
:stroke-width="14"
|
||||||
|
:text-inside="true"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
>{{ row.checks ? row.checks.filter((c) => c.checked).length : 0 }}/{{
|
||||||
|
row.checks ? row.checks.length : 0
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</el-progress>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="状态"
|
||||||
|
width="80"
|
||||||
|
:filters="[
|
||||||
|
{ text: '已完成', value: true },
|
||||||
|
{ text: '进行中', value: false },
|
||||||
|
]"
|
||||||
|
:filter-method="filterStatus"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.ended ? 'success' : 'warning'" size="small" effect="plain">
|
||||||
|
{{ row.ended ? '已完成' : '进行中' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="desc-text">{{ row.description || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="210" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="success" :disabled="row.ended" @click="doCheck(row)">
|
||||||
|
取出
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" :disabled="!isAdmin" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" :disabled="!isAdmin" @click="remove(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="editing ? '编辑稳定性实验' : '新增稳定性实验'"
|
||||||
|
width="620px"
|
||||||
|
align-center
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:show-close="false"
|
||||||
|
>
|
||||||
|
<el-form :model="form" label-position="top">
|
||||||
|
<el-form-item label="批次号" required>
|
||||||
|
<el-input v-model="form.batch" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="实验类型">
|
||||||
|
<el-radio-group v-model="form.type">
|
||||||
|
<el-radio value="长期">长期</el-radio>
|
||||||
|
<el-radio value="加速">加速</el-radio>
|
||||||
|
<el-radio value="其它">其它</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider>
|
||||||
|
<span style="font-size: 13px; color: var(--el-text-color-secondary)">检查点设置</span>
|
||||||
|
</el-divider>
|
||||||
|
|
||||||
|
<div class="check-add-row">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="newCheckDate"
|
||||||
|
type="date"
|
||||||
|
placeholder="选择日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
<el-input v-model="newCheckDesc" placeholder="描述(如:0个月)" style="width: 180px" />
|
||||||
|
<el-button type="primary" @click="addCheck">添加</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="form.checks" stripe size="small" style="margin-top: 12px">
|
||||||
|
<el-table-column label="日期" width="140">
|
||||||
|
<template #default="{ row: chk }">
|
||||||
|
{{ formatDate(chk.date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="description" label="描述" min-width="120" />
|
||||||
|
<el-table-column label="状态" width="80">
|
||||||
|
<template #default="{ row: chk }">
|
||||||
|
<el-tag :type="chk.checked ? 'success' : 'info'" size="small">
|
||||||
|
{{ chk.checked ? '已取出' : '待取出' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="60">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" link @click="removeCheck($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-container {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8896a8;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-unit {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #3d4a5c;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 步骤条(展开内容) ===== */
|
||||||
|
.expand-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-content .el-steps {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-desc {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-op {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-text {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-add-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
263
desktop-ui/src/views/Standard.vue
Normal file
263
desktop-ui/src/views/Standard.vue
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from "vue"
|
||||||
|
import { getCurrentInstance } from "vue"
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus"
|
||||||
|
|
||||||
|
const { $http } = getCurrentInstance().appContext.config.globalProperties
|
||||||
|
|
||||||
|
const isAdmin = localStorage.getItem('role') === 'admin'
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
if (!date) return "-"
|
||||||
|
const d = new Date(date)
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0")
|
||||||
|
const day = String(d.getDate()).padStart(2, "0")
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editing = ref(false)
|
||||||
|
const form = ref({
|
||||||
|
batch: "",
|
||||||
|
im: "",
|
||||||
|
ass: "",
|
||||||
|
calibration_date: "",
|
||||||
|
expire_date: "",
|
||||||
|
location: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 有效期筛查(天数,0=全部)
|
||||||
|
const filterDays = ref(0)
|
||||||
|
const filteredData = computed(() => {
|
||||||
|
if (!filterDays.value || filterDays.value <= 0) return data.value
|
||||||
|
const now = new Date()
|
||||||
|
const limit = new Date(now.getTime() + filterDays.value * 86400000)
|
||||||
|
return data.value.filter((item) => {
|
||||||
|
if (!item.expire_date) return false
|
||||||
|
const expire = new Date(item.expire_date)
|
||||||
|
return expire >= now && expire <= limit
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function rowClass({ row }) {
|
||||||
|
if (!row.expire_date) return ""
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const expire = new Date(row.expire_date)
|
||||||
|
expire.setHours(0, 0, 0, 0)
|
||||||
|
return expire < today ? "row-expired" : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowStyle({ row }) {
|
||||||
|
if (!row.expire_date) return {}
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const expire = new Date(row.expire_date)
|
||||||
|
expire.setHours(0, 0, 0, 0)
|
||||||
|
if (expire < today) {
|
||||||
|
return { color: "#dc2626" }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
const res = await $http.get("/standard")
|
||||||
|
data.value = res.data || []
|
||||||
|
} catch {
|
||||||
|
data.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editing.value = false
|
||||||
|
form.value = { batch: "", im: "", ass: "", calibration_date: "", expire_date: "", location: "" }
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
editing.value = true
|
||||||
|
form.value = { ...row }
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!form.value.batch) {
|
||||||
|
ElMessage.error("批号不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (editing.value) {
|
||||||
|
await $http.patch(`/standard/${form.value._id}`, form.value)
|
||||||
|
ElMessage.success("更新成功")
|
||||||
|
} else {
|
||||||
|
await $http.post("/standard", form.value)
|
||||||
|
ElMessage.success("创建成功")
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
await refresh()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || "操作失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除批号为"${row.batch}"的对照品吗?`, "确认删除", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
await $http.delete(`/standard/${row._id}`)
|
||||||
|
ElMessage.success("删除成功")
|
||||||
|
await refresh()
|
||||||
|
} catch {
|
||||||
|
// cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(refresh)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="actions-bar">
|
||||||
|
<div class="actions-left">
|
||||||
|
<el-button @click="refresh">刷新</el-button>
|
||||||
|
<el-button type="primary" @click="openCreate">新增对照品</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="actions-right">
|
||||||
|
<span class="filter-label">有效期</span>
|
||||||
|
<el-input-number v-model="filterDays" :min="0" :max="365" :step="7" size="small"
|
||||||
|
controls-position="right" style="width: 100px" />
|
||||||
|
<span class="filter-unit">天内到期</span>
|
||||||
|
<el-tag v-if="filterDays > 0" type="warning" size="small" effect="plain">
|
||||||
|
显示 {{ filteredData.length }} 条
|
||||||
|
</el-tag>
|
||||||
|
<el-button v-if="filterDays > 0" size="small" text type="info" @click="filterDays = 0">
|
||||||
|
清除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="filteredData" stripe border style="width: 100%" :row-class-name="rowClass"
|
||||||
|
:row-style="rowStyle">
|
||||||
|
<el-table-column prop="batch" label="批号" min-width="180" />
|
||||||
|
<el-table-column prop="im" label="含量(%)" width="100" />
|
||||||
|
<el-table-column prop="ass" label="纯度(%)" width="100" />
|
||||||
|
<el-table-column prop="calibration_date" label="标定日期" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.calibration_date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="expire_date" label="有效期" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.expire_date) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="location" label="存放位置" min-width="140" />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" :disabled="!isAdmin" @click="remove(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="editing ? '编辑对照品' : '新增对照品'" width="520px" align-center>
|
||||||
|
<el-form :model="form" label-position="top">
|
||||||
|
<el-form-item label="批号" required>
|
||||||
|
<el-input v-model="form.batch" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="含量(%)">
|
||||||
|
<el-input v-model="form.im" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="纯度(%)">
|
||||||
|
<el-input v-model="form.ass" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="标定日期">
|
||||||
|
<el-date-picker v-model="form.calibration_date" type="date" style="width: 100%"
|
||||||
|
value-format="YYYY-MM-DD" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="有效期">
|
||||||
|
<el-date-picker v-model="form.expire_date" type="date" style="width: 100%"
|
||||||
|
value-format="YYYY-MM-DD" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="存放位置">
|
||||||
|
<el-input v-model="form.location" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-container {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8896a8;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-unit {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #3d4a5c;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 过期行文字红色 */
|
||||||
|
:deep(.el-table .row-expired) {
|
||||||
|
color: #dc2626 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.row-expired:hover) {
|
||||||
|
background-color: #fee2e2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.row-expired .el-table__cell) {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
desktop-ui/vite.config.js
Normal file
18
desktop-ui/vite.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueDevTools(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
3
server/.gitignore
vendored
Normal file
3
server/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
http-test/
|
||||||
99
server/controller/stability.js
Normal file
99
server/controller/stability.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { requireAuth, requireAdmin } from './user.js'
|
||||||
|
import Stability from '../model/stability.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// 列出所有稳定性实验
|
||||||
|
router.get('/', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stabilities = await Stability.find().populate(['checks.checker', 'creater'])
|
||||||
|
res.json(stabilities)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 列出未完成的稳定性实验
|
||||||
|
router.get('/unend', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const statilities = await Stability.find({ ended: false }).populate(['checks.checker', 'creater'])
|
||||||
|
res.json(statilities)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建稳定性实验
|
||||||
|
router.post('/', requireAuth, async (req, res) => {
|
||||||
|
if (!req.body.batch) {
|
||||||
|
return res.status(400).json({ message: '缺少必要字段' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stability = await Stability.create({
|
||||||
|
creater: req.user.id,
|
||||||
|
...req.body,
|
||||||
|
})
|
||||||
|
if (stability) {
|
||||||
|
res.json(stability)
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ message: '资源创建失败' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除稳定性实验
|
||||||
|
router.delete('/:id', requireAuth, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stability = await Stability.findByIdAndDelete(req.params.id)
|
||||||
|
if (stability) {
|
||||||
|
res.json({ message: '删除成功' })
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ message: '删除失败' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 对 指定的稳定性实验进行check
|
||||||
|
// id:稳定性实验id
|
||||||
|
// checkid:检查点id
|
||||||
|
router.post('/check/:id/:checkid', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stability = await Stability.findById(req.params.id)
|
||||||
|
if (!stability) {
|
||||||
|
return res.status(404).json({ message: `未找到id为${req.params.id}的记录` })
|
||||||
|
}
|
||||||
|
stability.checks.forEach((check) => {
|
||||||
|
if (check._id == req.params.checkid) {
|
||||||
|
check.checked = true
|
||||||
|
check.checker = req.user.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
stability.ended = stability.checks.every((check) => check.checked)
|
||||||
|
await stability.save()
|
||||||
|
res.json(stability)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新稳定性实验
|
||||||
|
router.patch('/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stability = await Stability.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after' })
|
||||||
|
if (stability) {
|
||||||
|
res.json(stability)
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ message: `未找到id为${req.params.id}的记录` })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { router as stabilityRouter }
|
||||||
65
server/controller/standard.js
Normal file
65
server/controller/standard.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import Standard from '../model/standard.js'
|
||||||
|
import { requireAuth, requireAdmin } from './user.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// 获取对照品
|
||||||
|
router.get('/', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const standard = await Standard.find()
|
||||||
|
res.json(standard)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建对照品
|
||||||
|
router.post('/', requireAuth, async (req, res) => {
|
||||||
|
if (!req.body.batch) {
|
||||||
|
return res.status(400).json({ message: '缺少必要字段' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const standard = await Standard.create({ ...req.body })
|
||||||
|
if (standard) {
|
||||||
|
res.json(standard)
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ message: '资源创建失败' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除对照品
|
||||||
|
router.delete('/:id', requireAuth, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const standard = await Standard.findByIdAndDelete(req.params.id)
|
||||||
|
if (standard) {
|
||||||
|
res.json({ message: '删除成功' })
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ message: '删除成功' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '服务器错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新对照品
|
||||||
|
router.patch('/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log(req.params.id, req.body)
|
||||||
|
const standard = await Standard.findByIdAndUpdate(req.params.id, req.body, {
|
||||||
|
returnDocument: 'after',
|
||||||
|
})
|
||||||
|
res.json(standard)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
message: '服务器错误',
|
||||||
|
error: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { router as standardRouter }
|
||||||
103
server/controller/user.js
Normal file
103
server/controller/user.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import User from '../model/user.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// 获取所有用户
|
||||||
|
router.get('/', requireAuth, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = await User.find().select('-password')
|
||||||
|
res.json(users)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '获取用户信息时发生错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建新用户
|
||||||
|
router.post('/', requireAuth, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, nickname, password } = req.body
|
||||||
|
const user = await User.create({
|
||||||
|
username,
|
||||||
|
nickname,
|
||||||
|
password,
|
||||||
|
role: 'user',
|
||||||
|
})
|
||||||
|
res.status(201).json(user)
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: '创建用户时发生错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
router.delete('/:id', requireAuth, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (req.user._id.toString() === req.params.id) {
|
||||||
|
return res.status(400).json({ message: '不能删除你自己' })
|
||||||
|
}
|
||||||
|
const user = await User.findByIdAndDelete(req.params.id)
|
||||||
|
if (user) {
|
||||||
|
res.json({ message: '已删除用户' })
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ message: '该用户不存在' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '删除用户时发生错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 用户登录
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password } = req.body
|
||||||
|
const user = await User.findOne({ username })
|
||||||
|
if (user && (await user.comparePassword(password))) {
|
||||||
|
const token = jwt.sign({ id: user._id, username: user.username }, process.env.JWT_SECRET, {
|
||||||
|
expiresIn: '24h',
|
||||||
|
})
|
||||||
|
res.json({ token, user })
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ message: '用户名或密码错误' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ message: '登录过程中发生错误', error: error.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户身份,拒绝未认证的请求
|
||||||
|
*/
|
||||||
|
async function requireAuth(req, res, next) {
|
||||||
|
const authHeader = req.headers['authorization']
|
||||||
|
if (!authHeader) {
|
||||||
|
return res.status(401).json({ message: '未提供身份验证信息' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ').pop()
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ message: '提供了错误的身份验证信息' })
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET, async (err, decoded) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(401).json({ message: '身份验证失败' })
|
||||||
|
}
|
||||||
|
// 从数据库获取完整的用户信息
|
||||||
|
req.user = await User.findById(decoded.id)
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否具有管理员权限,调用前必须先调用requireAuth中间件验证用户身份
|
||||||
|
*/
|
||||||
|
async function requireAdmin(req, res, next) {
|
||||||
|
if (req.user && req.user.role === 'admin') {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
res.status(403).json({ message: '需要管理员权限' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { router as userRouter, requireAuth, requireAdmin }
|
||||||
24
server/database.js
Normal file
24
server/database.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
import User from './model/user.js'
|
||||||
|
|
||||||
|
const connectDB = async () => {
|
||||||
|
try {
|
||||||
|
const conn = await mongoose.connect(process.env.MONGO_URI)
|
||||||
|
console.log(`MongoDB Connected: ${conn.connection.host}`)
|
||||||
|
// 检查是否存在管理员用户,如果不存在则创建一个默认管理员
|
||||||
|
const adminUser = await User.findOne({ username: process.env.ADMIN_USERNAME })
|
||||||
|
if (!adminUser) {
|
||||||
|
await User.create({
|
||||||
|
username: process.env.ADMIN_USERNAME,
|
||||||
|
nickname: '管理员',
|
||||||
|
password: process.env.ADMIN_PASSWORD,
|
||||||
|
role: 'admin',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error connecting to MongoDB:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connectDB
|
||||||
26
server/index.js
Normal file
26
server/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import dotenv from 'dotenv'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
import connectDB from './database.js'
|
||||||
|
await connectDB()
|
||||||
|
|
||||||
|
import express from 'express'
|
||||||
|
import { userRouter } from './controller/user.js'
|
||||||
|
import { stabilityRouter } from './controller/stability.js'
|
||||||
|
import { standardRouter } from './controller/standard.js'
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
// 注册路由
|
||||||
|
app.use('/api/user', userRouter)
|
||||||
|
app.use('/api/stability', stabilityRouter)
|
||||||
|
app.use('/api/standard', standardRouter)
|
||||||
|
|
||||||
|
// 从环境变量中读取端口号,如果环境变量中没有指定端口号,则使用默认的端口号
|
||||||
|
const PORT = process.env.PORT || 5000
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server is running on http://localhost:${PORT}`)
|
||||||
|
})
|
||||||
79
server/model/stability.js
Normal file
79
server/model/stability.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
import User from './user.js'
|
||||||
|
|
||||||
|
const CheckSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
// 检查日期
|
||||||
|
date: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 是否已经检查
|
||||||
|
checked: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 检查人
|
||||||
|
checker: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
// 备注
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 稳定性实验 模型
|
||||||
|
const StabilitySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
// 实验批次号
|
||||||
|
batch: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 实验类型
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['长期', '加速', '其它'],
|
||||||
|
},
|
||||||
|
// 检查点列表
|
||||||
|
checks: {
|
||||||
|
type: [CheckSchema],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
// 是否完成
|
||||||
|
ended: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 创建人
|
||||||
|
creater: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 备注
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
StabilitySchema.pre('save', async function () {
|
||||||
|
try {
|
||||||
|
this.checks = this.checks.sort((a, b) => {
|
||||||
|
return new Date(a.date) - new Date(b.date)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Stability = mongoose.model('Stability', StabilitySchema)
|
||||||
|
|
||||||
|
export default Stability
|
||||||
31
server/model/standard.js
Normal file
31
server/model/standard.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const StandardSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
batch: {
|
||||||
|
type: String,
|
||||||
|
require: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
im: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
ass: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
calibration_date: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
expire_date: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const Standard = mongoose.model('Standard', StandardSchema)
|
||||||
|
|
||||||
|
export default Standard
|
||||||
47
server/model/user.js
Normal file
47
server/model/user.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
|
||||||
|
const UserSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
nickname: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
enum: ['admin', 'user'],
|
||||||
|
default: 'user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 保存前自动对密码进行哈希加密
|
||||||
|
UserSchema.pre('save', async function () {
|
||||||
|
this.password = await bcrypt.hash(this.password, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 实例方法:校验密码
|
||||||
|
UserSchema.methods.comparePassword = async function (candidatePassword) {
|
||||||
|
return bcrypt.compare(candidatePassword, this.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出时始终不返回密码
|
||||||
|
UserSchema.methods.toJSON = function () {
|
||||||
|
const obj = this.toObject()
|
||||||
|
delete obj.password
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
const User = mongoose.model('User', UserSchema)
|
||||||
|
|
||||||
|
export default User
|
||||||
1213
server/package-lock.json
generated
Normal file
1213
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
server/package.json
Normal file
19
server/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"author": "hbk01",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"mongoose": "^9.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user