This commit is contained in:
王子琦
2026-01-20 11:32:46 +08:00
parent bc4d194460
commit cc7d8f30ff
92 changed files with 5050 additions and 0 deletions

13
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<style>
body {
margin: 0;
background: #f3f8f3;
font-family: "Source Sans 3", "Helvetica Neue", Arial, sans-serif;
}
</style>

26
frontend/src/api/http.js Normal file
View File

@@ -0,0 +1,26 @@
import axios from "axios";
const http = axios.create({
baseURL: "/api"
});
http.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers["satoken"] = token;
}
return config;
});
http.interceptors.response.use(
(response) => {
const data = response.data;
if (data && data.code !== 0) {
return Promise.reject(new Error(data.message || "request error"));
}
return response;
},
(error) => Promise.reject(error)
);
export default http;

54
frontend/src/api/index.js Normal file
View File

@@ -0,0 +1,54 @@
import http from "./http";
export const login = (payload) => http.post("/auth/login", payload);
export const register = (payload) => http.post("/auth/register", payload);
export const me = () => http.get("/auth/me");
export const logout = () => http.post("/auth/logout");
export const adminStats = () => http.get("/admin/stats");
export const adminUsers = (role) => http.get("/admin/users", { params: { role } });
export const adminCreateUser = (payload) => http.post("/admin/users", payload);
export const adminUpdateUser = (payload) => http.put("/admin/users", payload);
export const adminResetPassword = (id, password) => http.post(`/admin/users/${id}/reset-password`, null, { params: { password } });
export const eldersList = () => http.get("/admin/elders");
export const eldersCreate = (payload) => http.post("/admin/elders", payload);
export const eldersUpdate = (payload) => http.put("/admin/elders", payload);
export const eldersDelete = (id) => http.delete(`/admin/elders/${id}`);
export const schedulesByDate = (date) => http.get("/admin/schedules", { params: { date } });
export const scheduleCreate = (payload) => http.post("/admin/schedules", payload);
export const scheduleUpdate = (payload) => http.put("/admin/schedules", payload);
export const scheduleDelete = (id) => http.delete(`/admin/schedules/${id}`);
export const billsCreate = (payload) => http.post("/admin/bills", payload);
export const billsList = (elderId) => http.get("/admin/bills", { params: { elderId } });
export const feedbackList = () => http.get("/admin/feedback");
export const feedbackUpdate = (payload) => http.put("/admin/feedback", payload);
export const noticeCreate = (payload) => http.post("/admin/notices", payload);
export const noticeList = (role, userId) => http.get("/admin/notices", { params: { role, userId } });
export const nurseSchedules = (date) => http.get("/nurse/schedules", { params: { date } });
export const nurseCareCreate = (payload) => http.post("/nurse/care-records", payload);
export const nurseCareList = (elderId) => http.get("/nurse/care-records", { params: { elderId } });
export const nurseHealthCreate = (payload) => http.post("/nurse/health-records", payload);
export const nurseHealthList = (elderId) => http.get("/nurse/health-records", { params: { elderId } });
export const nurseHandoverCreate = (payload) => http.post("/nurse/handovers", payload);
export const nurseHandoverList = () => http.get("/nurse/handovers");
export const nurseNoticeList = () => http.get("/nurse/notices");
export const familyElders = () => http.get("/family/elders");
export const familyCareList = (elderId) => http.get("/family/care-records", { params: { elderId } });
export const familyHealthList = (elderId) => http.get("/family/health-records", { params: { elderId } });
export const familyBills = (elderId) => http.get("/family/bills", { params: { elderId } });
export const familyPay = (id, payload) => http.post(`/family/bills/${id}/pay`, payload);
export const familyFeedback = (payload) => http.post("/family/feedback", payload);
export const familyNoticeList = () => http.get("/family/notices");
export const uploadFile = (file) => {
const form = new FormData();
form.append("file", file);
return http.post("/files/upload", form, { headers: { "Content-Type": "multipart/form-data" } });
};

View File

@@ -0,0 +1,34 @@
:root {
--primary-green: #2f7d59;
--primary-green-dark: #225c43;
--primary-green-light: #d9efe3;
}
.el-button--primary,
.el-menu-item.is-active,
.el-submenu__title:hover,
.el-menu-item:hover {
background-color: var(--primary-green) !important;
border-color: var(--primary-green) !important;
color: #fff !important;
}
.el-menu {
border-right: none;
}
.page-card {
background: #fff;
border-radius: 10px;
padding: 16px;
box-shadow: 0 8px 24px rgba(47, 125, 89, 0.12);
}
.header-bar {
background: linear-gradient(135deg, var(--primary-green) 0%, #5fbf90 100%);
color: #fff;
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}

14
frontend/src/main.js Normal file
View File

@@ -0,0 +1,14 @@
import Vue from "vue";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import "./assets/theme.css";
import App from "./App.vue";
import router from "./router";
Vue.use(ElementUI);
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App)
}).$mount("#app");

View File

@@ -0,0 +1,102 @@
import Vue from "vue";
import Router from "vue-router";
import Login from "../views/Login.vue";
import Register from "../views/Register.vue";
import Layout from "../views/Layout.vue";
import AdminDashboard from "../views/admin/Dashboard.vue";
import AdminUsers from "../views/admin/Users.vue";
import AdminElders from "../views/admin/Elders.vue";
import AdminSchedules from "../views/admin/Schedules.vue";
import AdminBills from "../views/admin/Bills.vue";
import AdminFeedback from "../views/admin/Feedback.vue";
import AdminNotices from "../views/admin/Notices.vue";
import NurseDashboard from "../views/nurse/Dashboard.vue";
import NurseSchedules from "../views/nurse/Schedules.vue";
import NurseCare from "../views/nurse/CareRecords.vue";
import NurseHealth from "../views/nurse/HealthRecords.vue";
import NurseHandovers from "../views/nurse/Handovers.vue";
import NurseNotices from "../views/nurse/Notices.vue";
import FamilyDashboard from "../views/family/Dashboard.vue";
import FamilyElders from "../views/family/Elders.vue";
import FamilyCare from "../views/family/CareRecords.vue";
import FamilyHealth from "../views/family/HealthRecords.vue";
import FamilyBills from "../views/family/Bills.vue";
import FamilyFeedback from "../views/family/Feedback.vue";
import FamilyNotices from "../views/family/Notices.vue";
Vue.use(Router);
const router = new Router({
mode: "history",
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{ path: "/register", component: Register },
{
path: "/admin",
component: Layout,
meta: { role: "ADMIN" },
children: [
{ path: "", redirect: "dashboard" },
{ path: "dashboard", component: AdminDashboard },
{ path: "users", component: AdminUsers },
{ path: "elders", component: AdminElders },
{ path: "schedules", component: AdminSchedules },
{ path: "bills", component: AdminBills },
{ path: "feedback", component: AdminFeedback },
{ path: "notices", component: AdminNotices }
]
},
{
path: "/nurse",
component: Layout,
meta: { role: "NURSE" },
children: [
{ path: "", redirect: "dashboard" },
{ path: "dashboard", component: NurseDashboard },
{ path: "schedules", component: NurseSchedules },
{ path: "care", component: NurseCare },
{ path: "health", component: NurseHealth },
{ path: "handovers", component: NurseHandovers },
{ path: "notices", component: NurseNotices }
]
},
{
path: "/family",
component: Layout,
meta: { role: "FAMILY" },
children: [
{ path: "", redirect: "dashboard" },
{ path: "dashboard", component: FamilyDashboard },
{ path: "elders", component: FamilyElders },
{ path: "care", component: FamilyCare },
{ path: "health", component: FamilyHealth },
{ path: "bills", component: FamilyBills },
{ path: "feedback", component: FamilyFeedback },
{ path: "notices", component: FamilyNotices }
]
}
]
});
router.beforeEach((to, from, next) => {
const token = localStorage.getItem("token");
const role = localStorage.getItem("role");
if (to.path === "/login" || to.path === "/register") {
return next();
}
if (!token) {
return next("/login");
}
if (to.meta && to.meta.role && to.meta.role !== role) {
if (role === "ADMIN") return next("/admin");
if (role === "NURSE") return next("/nurse");
if (role === "FAMILY") return next("/family");
}
return next();
});
export default router;

View File

@@ -0,0 +1,20 @@
export function formatDate(value) {
if (!value) return "";
const d = new Date(value);
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}`;
}
export function formatDateTime(value) {
if (!value) return "";
const d = new Date(value);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
const ss = String(d.getSeconds()).padStart(2, "0");
return `${y}-${m}-${day}T${hh}:${mm}:${ss}`;
}

View File

@@ -0,0 +1,96 @@
<template>
<el-container style="min-height: 100vh;">
<el-aside width="220px" style="background: #0f2f23; color: #fff;">
<div style="padding: 18px 16px; font-size: 18px; font-weight: 600;">
Nursing Home
</div>
<el-menu
:default-active="activeMenu"
router
background-color="#0f2f23"
text-color="#c7e7d5"
active-text-color="#ffffff">
<el-menu-item v-for="item in menu" :key="item.path" :index="item.path">
{{ item.label }}
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<div class="header-bar">
<div>{{ roleLabel }}</div>
<div>
<el-button size="mini" type="primary" @click="handleLogout">Logout</el-button>
</div>
</div>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script>
import { logout } from "../api";
export default {
data() {
return {
role: localStorage.getItem("role")
};
},
computed: {
menu() {
if (this.role === "ADMIN") {
return [
{ path: "/admin/dashboard", label: "Dashboard" },
{ path: "/admin/users", label: "Users" },
{ path: "/admin/elders", label: "Elders" },
{ path: "/admin/schedules", label: "Schedules" },
{ path: "/admin/bills", label: "Bills" },
{ path: "/admin/feedback", label: "Feedback" },
{ path: "/admin/notices", label: "Notices" }
];
}
if (this.role === "NURSE") {
return [
{ path: "/nurse/dashboard", label: "Dashboard" },
{ path: "/nurse/schedules", label: "My Schedule" },
{ path: "/nurse/care", label: "Care Records" },
{ path: "/nurse/health", label: "Health Records" },
{ path: "/nurse/handovers", label: "Handovers" },
{ path: "/nurse/notices", label: "Notices" }
];
}
return [
{ path: "/family/dashboard", label: "Dashboard" },
{ path: "/family/elders", label: "Elders" },
{ path: "/family/care", label: "Daily Care" },
{ path: "/family/health", label: "Health" },
{ path: "/family/bills", label: "Bills" },
{ path: "/family/feedback", label: "Feedback" },
{ path: "/family/notices", label: "Notices" }
];
},
activeMenu() {
return this.$route.path;
},
roleLabel() {
if (this.role === "ADMIN") return "Administrator";
if (this.role === "NURSE") return "Nurse";
return "Family";
}
},
methods: {
async handleLogout() {
try {
await logout();
} catch (e) {
// ignore
}
localStorage.removeItem("token");
localStorage.removeItem("role");
this.$router.push("/login");
}
}
};
</script>

View File

@@ -0,0 +1,75 @@
<template>
<div class="login-page">
<div class="login-card">
<h2>Welcome Back</h2>
<el-form :model="form" label-position="top">
<el-form-item label="Username">
<el-input v-model="form.username" placeholder="username"></el-input>
</el-form-item>
<el-form-item label="Password">
<el-input v-model="form.password" type="password" placeholder="password"></el-input>
</el-form-item>
<el-button type="primary" style="width: 100%;" @click="handleLogin">Login</el-button>
</el-form>
<div class="login-footer">
<span>No account?</span>
<el-button type="text" @click="$router.push('/register')">Register</el-button>
</div>
</div>
</div>
</template>
<script>
import { login } from "../api";
export default {
data() {
return {
form: {
username: "",
password: ""
}
};
},
methods: {
async handleLogin() {
try {
const res = await login(this.form);
const data = res.data.data;
localStorage.setItem("token", data.token);
localStorage.setItem("role", data.role);
if (data.role === "ADMIN") {
this.$router.push("/admin");
} else if (data.role === "NURSE") {
this.$router.push("/nurse");
} else {
this.$router.push("/family");
}
} catch (e) {
this.$message.error(e.message || "login failed");
}
}
}
};
</script>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at 20% 20%, #dff3e7, #f3f8f3);
}
.login-card {
width: 360px;
padding: 28px;
background: #fff;
border-radius: 12px;
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
}
.login-footer {
margin-top: 12px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="login-page">
<div class="login-card">
<h2>Family Register</h2>
<el-form :model="form" label-position="top">
<el-form-item label="Username">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="Password">
<el-input v-model="form.password" type="password"></el-input>
</el-form-item>
<el-form-item label="Name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="Phone">
<el-input v-model="form.phone"></el-input>
</el-form-item>
<el-form-item label="Elder ID Card">
<el-input v-model="form.elderIdCard"></el-input>
</el-form-item>
<el-form-item label="Relationship">
<el-input v-model="form.relationship" placeholder="son/daughter/spouse"></el-input>
</el-form-item>
<el-button type="primary" style="width: 100%;" @click="handleRegister">Register</el-button>
</el-form>
<div class="login-footer">
<el-button type="text" @click="$router.push('/login')">Back to Login</el-button>
</div>
</div>
</div>
</template>
<script>
import { register } from "../api";
export default {
data() {
return {
form: {
username: "",
password: "",
name: "",
phone: "",
elderIdCard: "",
relationship: ""
}
};
},
methods: {
async handleRegister() {
try {
await register(this.form);
this.$message.success("registered");
this.$router.push("/login");
} catch (e) {
this.$message.error(e.message || "register failed");
}
}
}
};
</script>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at 20% 20%, #dff3e7, #f3f8f3);
}
.login-card {
width: 420px;
padding: 28px;
background: #fff;
border-radius: 12px;
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
}
.login-footer {
margin-top: 12px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="page-card">
<h3>Billing</h3>
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
<el-input v-model="filterElderId" placeholder="Elder ID" style="width: 200px;" />
<el-button type="primary" @click="load">Search</el-button>
<el-button @click="showCreate = true">New Bill</el-button>
</div>
<el-table :data="bills" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="elderId" label="Elder ID" width="100" />
<el-table-column prop="month" label="Month" />
<el-table-column prop="total" label="Total" />
<el-table-column prop="status" label="Status" />
</el-table>
<el-dialog title="New Bill" :visible.sync="showCreate">
<el-form :model="form" label-width="120px">
<el-form-item label="Elder ID"><el-input v-model="form.elderId"/></el-form-item>
<el-form-item label="Month"><el-input v-model="form.month" placeholder="YYYY-MM"/></el-form-item>
<el-form-item label="Bed Fee"><el-input v-model="form.bedFee"/></el-form-item>
<el-form-item label="Care Fee"><el-input v-model="form.careFee"/></el-form-item>
<el-form-item label="Meal Fee"><el-input v-model="form.mealFee"/></el-form-item>
<el-form-item label="Other Fee"><el-input v-model="form.otherFee"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showCreate = false">Cancel</el-button>
<el-button type="primary" @click="create">Create</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { billsList, billsCreate } from "../../api";
export default {
data() {
return {
bills: [],
filterElderId: "",
showCreate: false,
form: { elderId: "", month: "", bedFee: "", careFee: "", mealFee: "", otherFee: "" }
};
},
created() {
this.load();
},
methods: {
async load() {
try {
const res = await billsList(this.filterElderId || null);
this.bills = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
async create() {
try {
await billsCreate(this.form);
this.showCreate = false;
this.load();
} catch (e) {
this.$message.error(e.message || "create failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div class="page-card">
<h3>Admin Dashboard</h3>
<el-row :gutter="16" style="margin-top: 12px;">
<el-col :span="6">
<el-card>
<div>Elders</div>
<h2>{{ stats.elders }}</h2>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div>Nurses</div>
<h2>{{ stats.nurses }}</h2>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div>Families</div>
<h2>{{ stats.families }}</h2>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div>Income</div>
<h2>{{ stats.income }}</h2>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { adminStats } from "../../api";
export default {
data() {
return { stats: { elders: 0, nurses: 0, families: 0, income: 0 } };
},
async created() {
try {
const res = await adminStats();
this.stats = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
};
</script>

View File

@@ -0,0 +1,128 @@
<template>
<div class="page-card">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Elders</h3>
<el-button type="primary" @click="showCreate = true">Add Elder</el-button>
</div>
<el-table :data="elders" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="gender" label="Gender" width="80" />
<el-table-column prop="idCard" label="ID Card" />
<el-table-column prop="roomNo" label="Room" width="100" />
<el-table-column prop="careLevel" label="Care Level" />
<el-table-column label="Actions" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="editElder(scope.row)">Edit</el-button>
<el-button size="mini" type="danger" @click="deleteElder(scope.row)">Delete</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="Add Elder" :visible.sync="showCreate">
<el-form :model="form" label-width="120px">
<el-form-item label="Name"><el-input v-model="form.name"/></el-form-item>
<el-form-item label="Gender"><el-input v-model="form.gender"/></el-form-item>
<el-form-item label="ID Card"><el-input v-model="form.idCard"/></el-form-item>
<el-form-item label="Birthday"><el-date-picker v-model="form.birthday" type="date"/></el-form-item>
<el-form-item label="Room No"><el-input v-model="form.roomNo"/></el-form-item>
<el-form-item label="Check In"><el-date-picker v-model="form.checkInDate" type="date"/></el-form-item>
<el-form-item label="Care Level"><el-input v-model="form.careLevel"/></el-form-item>
<el-form-item label="Status"><el-input v-model="form.status"/></el-form-item>
<el-form-item label="Remark"><el-input v-model="form.remark"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showCreate = false">Cancel</el-button>
<el-button type="primary" @click="createElder">Create</el-button>
</span>
</el-dialog>
<el-dialog title="Edit Elder" :visible.sync="showEdit">
<el-form :model="editForm" label-width="120px">
<el-form-item label="Name"><el-input v-model="editForm.name"/></el-form-item>
<el-form-item label="Gender"><el-input v-model="editForm.gender"/></el-form-item>
<el-form-item label="ID Card"><el-input v-model="editForm.idCard"/></el-form-item>
<el-form-item label="Birthday"><el-date-picker v-model="editForm.birthday" type="date"/></el-form-item>
<el-form-item label="Room No"><el-input v-model="editForm.roomNo"/></el-form-item>
<el-form-item label="Check In"><el-date-picker v-model="editForm.checkInDate" type="date"/></el-form-item>
<el-form-item label="Care Level"><el-input v-model="editForm.careLevel"/></el-form-item>
<el-form-item label="Status"><el-input v-model="editForm.status"/></el-form-item>
<el-form-item label="Remark"><el-input v-model="editForm.remark"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showEdit = false">Cancel</el-button>
<el-button type="primary" @click="updateElder">Save</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { eldersList, eldersCreate, eldersUpdate, eldersDelete } from "../../api";
import { formatDate } from "../../utils/date";
export default {
data() {
return {
elders: [],
showCreate: false,
showEdit: false,
form: { name: "", gender: "", idCard: "", birthday: "", roomNo: "", checkInDate: "", careLevel: "", status: "", remark: "" },
editForm: {}
};
},
created() {
this.load();
},
methods: {
async load() {
try {
const res = await eldersList();
this.elders = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
async createElder() {
try {
const payload = {
...this.form,
birthday: formatDate(this.form.birthday),
checkInDate: formatDate(this.form.checkInDate)
};
await eldersCreate(payload);
this.showCreate = false;
this.load();
} catch (e) {
this.$message.error(e.message || "create failed");
}
},
editElder(row) {
this.editForm = { ...row };
this.showEdit = true;
},
async updateElder() {
try {
const payload = {
...this.editForm,
birthday: formatDate(this.editForm.birthday),
checkInDate: formatDate(this.editForm.checkInDate)
};
await eldersUpdate(payload);
this.showEdit = false;
this.load();
} catch (e) {
this.$message.error(e.message || "update failed");
}
},
async deleteElder(row) {
try {
await eldersDelete(row.id);
this.load();
} catch (e) {
this.$message.error(e.message || "delete failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,76 @@
<template>
<div class="page-card">
<h3>Feedback</h3>
<el-table :data="items" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="elderId" label="Elder ID" />
<el-table-column prop="type" label="Type" />
<el-table-column prop="content" label="Content" />
<el-table-column prop="status" label="Status" width="120" />
<el-table-column label="Actions" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="openReply(scope.row)">Reply</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="Reply" :visible.sync="showReply">
<el-form :model="replyForm" label-width="120px">
<el-form-item label="Status">
<el-select v-model="replyForm.status">
<el-option label="New" value="NEW" />
<el-option label="Processing" value="PROCESSING" />
<el-option label="Closed" value="CLOSED" />
</el-select>
</el-form-item>
<el-form-item label="Reply">
<el-input v-model="replyForm.reply" type="textarea" />
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showReply = false">Cancel</el-button>
<el-button type="primary" @click="saveReply">Save</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { feedbackList, feedbackUpdate } from "../../api";
export default {
data() {
return {
items: [],
showReply: false,
replyForm: { id: null, status: "PROCESSING", reply: "" }
};
},
created() {
this.load();
},
methods: {
async load() {
try {
const res = await feedbackList();
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
openReply(row) {
this.replyForm = { id: row.id, status: row.status || "PROCESSING", reply: row.reply || "" };
this.showReply = true;
},
async saveReply() {
try {
await feedbackUpdate(this.replyForm);
this.showReply = false;
this.load();
} catch (e) {
this.$message.error(e.message || "update failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div class="page-card">
<h3>Notices</h3>
<div style="margin-bottom: 12px;">
<el-button type="primary" @click="showCreate = true">New Notice</el-button>
</div>
<el-table :data="items" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="Title" />
<el-table-column prop="targetRole" label="Target" width="120" />
<el-table-column prop="createdAt" label="Created" />
</el-table>
<el-dialog title="New Notice" :visible.sync="showCreate">
<el-form :model="form" label-width="120px">
<el-form-item label="Title"><el-input v-model="form.title"/></el-form-item>
<el-form-item label="Content"><el-input v-model="form.content" type="textarea"/></el-form-item>
<el-form-item label="Target Role">
<el-select v-model="form.targetRole">
<el-option label="All" value="ALL" />
<el-option label="Nurse" value="NURSE" />
<el-option label="Family" value="FAMILY" />
</el-select>
</el-form-item>
<el-form-item label="Target User ID"><el-input v-model="form.targetUserId"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showCreate = false">Cancel</el-button>
<el-button type="primary" @click="create">Create</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { noticeCreate, noticeList } from "../../api";
export default {
data() {
return {
items: [],
showCreate: false,
form: { title: "", content: "", targetRole: "ALL", targetUserId: "" }
};
},
created() {
this.load();
},
methods: {
async load() {
try {
const res = await noticeList();
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
async create() {
try {
const payload = { ...this.form, targetUserId: this.form.targetUserId || null };
await noticeCreate(payload);
this.showCreate = false;
this.load();
} catch (e) {
this.$message.error(e.message || "create failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,108 @@
<template>
<div class="page-card">
<h3>Schedule Management</h3>
<div style="margin: 12px 0; display:flex; gap: 12px;">
<el-date-picker v-model="date" type="date" placeholder="Pick date" />
<el-button type="primary" @click="load">Search</el-button>
<el-button @click="showCreate = true">Add Schedule</el-button>
</div>
<el-table :data="schedules" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="nurseId" label="Nurse ID" width="100" />
<el-table-column prop="date" label="Date" />
<el-table-column prop="shift" label="Shift" />
<el-table-column prop="task" label="Task" />
<el-table-column label="Actions" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="edit(scope.row)">Edit</el-button>
<el-button size="mini" type="danger" @click="remove(scope.row)">Delete</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="Schedule" :visible.sync="showCreate">
<el-form :model="form" label-width="120px">
<el-form-item label="Nurse ID"><el-input v-model="form.nurseId"/></el-form-item>
<el-form-item label="Date"><el-date-picker v-model="form.date" type="date"/></el-form-item>
<el-form-item label="Shift"><el-input v-model="form.shift"/></el-form-item>
<el-form-item label="Task"><el-input v-model="form.task"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showCreate = false">Cancel</el-button>
<el-button type="primary" @click="create">Save</el-button>
</span>
</el-dialog>
<el-dialog title="Edit Schedule" :visible.sync="showEdit">
<el-form :model="editForm" label-width="120px">
<el-form-item label="Nurse ID"><el-input v-model="editForm.nurseId"/></el-form-item>
<el-form-item label="Date"><el-date-picker v-model="editForm.date" type="date"/></el-form-item>
<el-form-item label="Shift"><el-input v-model="editForm.shift"/></el-form-item>
<el-form-item label="Task"><el-input v-model="editForm.task"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showEdit = false">Cancel</el-button>
<el-button type="primary" @click="update">Save</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { schedulesByDate, scheduleCreate, scheduleUpdate, scheduleDelete } from "../../api";
import { formatDate } from "../../utils/date";
export default {
data() {
return {
date: "",
schedules: [],
showCreate: false,
showEdit: false,
form: { nurseId: "", date: "", shift: "", task: "" },
editForm: {}
};
},
methods: {
async load() {
if (!this.date) return;
try {
const res = await schedulesByDate(formatDate(this.date));
this.schedules = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
async create() {
try {
await scheduleCreate({ ...this.form, date: formatDate(this.form.date) });
this.showCreate = false;
this.load();
} catch (e) {
this.$message.error(e.message || "create failed");
}
},
edit(row) {
this.editForm = { ...row };
this.showEdit = true;
},
async update() {
try {
await scheduleUpdate({ ...this.editForm, date: formatDate(this.editForm.date) });
this.showEdit = false;
this.load();
} catch (e) {
this.$message.error(e.message || "update failed");
}
},
async remove(row) {
try {
await scheduleDelete(row.id);
this.load();
} catch (e) {
this.$message.error(e.message || "delete failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,132 @@
<template>
<div class="page-card">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>User Management</h3>
<el-button type="primary" @click="showCreate = true">New User</el-button>
</div>
<div style="margin: 12px 0;">
<el-select v-model="role" placeholder="role" @change="loadUsers">
<el-option label="All" value=""></el-option>
<el-option label="Admin" value="ADMIN"></el-option>
<el-option label="Nurse" value="NURSE"></el-option>
<el-option label="Family" value="FAMILY"></el-option>
</el-select>
</div>
<el-table :data="users" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="Username" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="role" label="Role" width="100" />
<el-table-column prop="status" label="Status" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
{{ scope.row.status === 1 ? 'Active' : 'Disabled' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="Actions" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="editUser(scope.row)">Edit</el-button>
<el-button size="mini" @click="resetPwd(scope.row)">Reset</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="Create User" :visible.sync="showCreate">
<el-form :model="createForm" label-width="120px">
<el-form-item label="Username"><el-input v-model="createForm.username"/></el-form-item>
<el-form-item label="Password"><el-input v-model="createForm.password" type="password"/></el-form-item>
<el-form-item label="Name"><el-input v-model="createForm.name"/></el-form-item>
<el-form-item label="Phone"><el-input v-model="createForm.phone"/></el-form-item>
<el-form-item label="Role">
<el-select v-model="createForm.role">
<el-option label="Admin" value="ADMIN"/>
<el-option label="Nurse" value="NURSE"/>
<el-option label="Family" value="FAMILY"/>
</el-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showCreate = false">Cancel</el-button>
<el-button type="primary" @click="createUser">Create</el-button>
</span>
</el-dialog>
<el-dialog title="Edit User" :visible.sync="showEdit">
<el-form :model="editForm" label-width="120px">
<el-form-item label="Name"><el-input v-model="editForm.name"/></el-form-item>
<el-form-item label="Phone"><el-input v-model="editForm.phone"/></el-form-item>
<el-form-item label="Status">
<el-select v-model="editForm.status">
<el-option label="Active" :value="1"/>
<el-option label="Disabled" :value="0"/>
</el-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showEdit = false">Cancel</el-button>
<el-button type="primary" @click="updateUser">Save</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { adminUsers, adminCreateUser, adminUpdateUser, adminResetPassword } from "../../api";
export default {
data() {
return {
role: "",
users: [],
showCreate: false,
showEdit: false,
createForm: { username: "", password: "", name: "", phone: "", role: "NURSE" },
editForm: { id: null, name: "", phone: "", status: 1 }
};
},
created() {
this.loadUsers();
},
methods: {
async loadUsers() {
try {
const res = await adminUsers(this.role || null);
this.users = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
async createUser() {
try {
await adminCreateUser(this.createForm);
this.showCreate = false;
this.loadUsers();
} catch (e) {
this.$message.error(e.message || "create failed");
}
},
editUser(row) {
this.editForm = { id: row.id, name: row.name, phone: row.phone, status: row.status };
this.showEdit = true;
},
async updateUser() {
try {
await adminUpdateUser(this.editForm);
this.showEdit = false;
this.loadUsers();
} catch (e) {
this.$message.error(e.message || "update failed");
}
},
async resetPwd(row) {
try {
await adminResetPassword(row.id, "123456");
this.$message.success("reset to 123456");
} catch (e) {
this.$message.error(e.message || "reset failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,60 @@
<template>
<div class="page-card">
<h3>Bills</h3>
<div style="margin-bottom: 12px;">
<el-select v-model="elderId" placeholder="Select elder" @change="load">
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
</el-select>
</div>
<el-table :data="items" stripe>
<el-table-column prop="month" label="Month" />
<el-table-column prop="total" label="Total" />
<el-table-column prop="status" label="Status" />
<el-table-column label="Actions" width="160">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="pay(scope.row)" :disabled="scope.row.status === 'PAID'">
Pay
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { familyElders, familyBills, familyPay } from "../../api";
export default {
data() {
return { elders: [], elderId: "", items: [] };
},
async created() {
try {
const res = await familyElders();
this.elders = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
methods: {
async load() {
if (!this.elderId) return;
try {
const res = await familyBills(this.elderId);
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
async pay(row) {
try {
await familyPay(row.id, { method: "ONLINE" });
this.$message.success("paid");
this.load();
} catch (e) {
this.$message.error(e.message || "pay failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,44 @@
<template>
<div class="page-card">
<h3>Daily Care</h3>
<div style="margin-bottom: 12px;">
<el-select v-model="elderId" placeholder="Select elder" @change="load">
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
</el-select>
</div>
<el-table :data="items" stripe>
<el-table-column prop="recordTime" label="Time" />
<el-table-column prop="content" label="Content" />
<el-table-column prop="attachmentUrl" label="Attachment" />
</el-table>
</div>
</template>
<script>
import { familyElders, familyCareList } from "../../api";
export default {
data() {
return { elders: [], elderId: "", items: [] };
},
async created() {
try {
const res = await familyElders();
this.elders = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
methods: {
async load() {
if (!this.elderId) return;
try {
const res = await familyCareList(this.elderId);
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,6 @@
<template>
<div class="page-card">
<h3>Family Dashboard</h3>
<p>View elder status, bills, and send feedback.</p>
</div>
</template>

View File

@@ -0,0 +1,29 @@
<template>
<div class="page-card">
<h3>Elders</h3>
<el-table :data="elders" stripe>
<el-table-column prop="name" label="Name" />
<el-table-column prop="gender" label="Gender" width="80" />
<el-table-column prop="roomNo" label="Room" width="100" />
<el-table-column prop="careLevel" label="Care Level" />
</el-table>
</div>
</template>
<script>
import { familyElders } from "../../api";
export default {
data() {
return { elders: [] };
},
async created() {
try {
const res = await familyElders();
this.elders = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
};
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div class="page-card">
<h3>Feedback</h3>
<el-form :model="form" label-width="120px">
<el-form-item label="Elder">
<el-select v-model="form.elderId" placeholder="Select elder">
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
</el-select>
</el-form-item>
<el-form-item label="Type">
<el-select v-model="form.type">
<el-option label="Suggestion" value="SUGGESTION" />
<el-option label="Complaint" value="COMPLAINT" />
<el-option label="Praise" value="PRAISE" />
</el-select>
</el-form-item>
<el-form-item label="Content">
<el-input v-model="form.content" type="textarea" />
</el-form-item>
<el-form-item label="Rating">
<el-input v-model="form.rating" />
</el-form-item>
<el-button type="primary" @click="submit">Submit</el-button>
</el-form>
</div>
</template>
<script>
import { familyElders, familyFeedback } from "../../api";
export default {
data() {
return {
elders: [],
form: { elderId: "", type: "SUGGESTION", content: "", rating: "" }
};
},
async created() {
try {
const res = await familyElders();
this.elders = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
methods: {
async submit() {
try {
await familyFeedback(this.form);
this.$message.success("submitted");
} catch (e) {
this.$message.error(e.message || "submit failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,46 @@
<template>
<div class="page-card">
<h3>Health Records</h3>
<div style="margin-bottom: 12px;">
<el-select v-model="elderId" placeholder="Select elder" @change="load">
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
</el-select>
</div>
<el-table :data="items" stripe>
<el-table-column prop="recordTime" label="Time" />
<el-table-column prop="temperature" label="Temp" />
<el-table-column prop="bpSystolic" label="BP S" />
<el-table-column prop="bpDiastolic" label="BP D" />
<el-table-column prop="heartRate" label="HR" />
</el-table>
</div>
</template>
<script>
import { familyElders, familyHealthList } from "../../api";
export default {
data() {
return { elders: [], elderId: "", items: [] };
},
async created() {
try {
const res = await familyElders();
this.elders = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
methods: {
async load() {
if (!this.elderId) return;
try {
const res = await familyHealthList(this.elderId);
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="page-card">
<h3>Notices</h3>
<el-table :data="items" stripe>
<el-table-column prop="title" label="Title" />
<el-table-column prop="content" label="Content" />
<el-table-column prop="createdAt" label="Created" />
</el-table>
</div>
</template>
<script>
import { familyNoticeList } from "../../api";
export default {
data() {
return { items: [] };
},
async created() {
try {
const res = await familyNoticeList();
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
};
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div class="page-card">
<h3>Care Records</h3>
<el-form :model="form" label-width="120px">
<el-form-item label="Elder ID"><el-input v-model="form.elderId"/></el-form-item>
<el-form-item label="Content"><el-input v-model="form.content" type="textarea"/></el-form-item>
<el-form-item label="Record Time"><el-date-picker v-model="form.recordTime" type="datetime"/></el-form-item>
<el-form-item label="Attachment">
<el-upload :http-request="upload" :show-file-list="false">
<el-button size="mini">Upload</el-button>
</el-upload>
<div v-if="form.attachmentUrl" style="margin-top: 6px;">
Uploaded: {{ form.attachmentUrl }}
</div>
</el-form-item>
<el-button type="primary" @click="create">Save</el-button>
</el-form>
<div style="margin-top: 16px;">
<el-input v-model="filterElderId" placeholder="Elder ID" style="width: 200px;" />
<el-button @click="load" style="margin-left: 8px;">Search</el-button>
<el-table :data="items" stripe style="margin-top: 12px;">
<el-table-column prop="elderId" label="Elder ID" width="100" />
<el-table-column prop="content" label="Content" />
<el-table-column prop="recordTime" label="Record Time" />
</el-table>
</div>
</div>
</template>
<script>
import { nurseCareCreate, nurseCareList, uploadFile } from "../../api";
import { formatDateTime } from "../../utils/date";
export default {
data() {
return {
form: { elderId: "", content: "", recordTime: "", attachmentUrl: "" },
filterElderId: "",
items: []
};
},
methods: {
async upload(option) {
try {
const res = await uploadFile(option.file);
this.form.attachmentUrl = res.data.data.url;
} catch (e) {
this.$message.error(e.message || "upload failed");
}
},
async create() {
try {
const payload = { ...this.form, recordTime: formatDateTime(this.form.recordTime) };
await nurseCareCreate(payload);
this.$message.success("saved");
} catch (e) {
this.$message.error(e.message || "save failed");
}
},
async load() {
if (!this.filterElderId) return;
try {
const res = await nurseCareList(this.filterElderId);
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,6 @@
<template>
<div class="page-card">
<h3>Nurse Dashboard</h3>
<p>Use the menu to manage schedules and records.</p>
</div>
</template>

View File

@@ -0,0 +1,52 @@
<template>
<div class="page-card">
<h3>Handovers</h3>
<el-form :model="form" label-width="120px">
<el-form-item label="Date"><el-date-picker v-model="form.date" type="date"/></el-form-item>
<el-form-item label="Content"><el-input v-model="form.content" type="textarea"/></el-form-item>
<el-button type="primary" @click="create">Save</el-button>
</el-form>
<el-table :data="items" stripe style="margin-top: 12px;">
<el-table-column prop="date" label="Date" />
<el-table-column prop="content" label="Content" />
</el-table>
</div>
</template>
<script>
import { nurseHandoverCreate, nurseHandoverList } from "../../api";
import { formatDate } from "../../utils/date";
export default {
data() {
return {
form: { date: "", content: "" },
items: []
};
},
created() {
this.load();
},
methods: {
async load() {
try {
const res = await nurseHandoverList();
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
},
async create() {
try {
const payload = { ...this.form, date: formatDate(this.form.date) };
await nurseHandoverCreate(payload);
this.$message.success("saved");
this.load();
} catch (e) {
this.$message.error(e.message || "save failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div class="page-card">
<h3>Health Records</h3>
<el-form :model="form" label-width="140px">
<el-form-item label="Elder ID"><el-input v-model="form.elderId"/></el-form-item>
<el-form-item label="Temperature"><el-input v-model="form.temperature"/></el-form-item>
<el-form-item label="Blood Pressure">
<div style="display:flex; gap:8px;">
<el-input v-model="form.bpSystolic" placeholder="Systolic" />
<el-input v-model="form.bpDiastolic" placeholder="Diastolic" />
</div>
</el-form-item>
<el-form-item label="Heart Rate"><el-input v-model="form.heartRate"/></el-form-item>
<el-form-item label="Note"><el-input v-model="form.note"/></el-form-item>
<el-form-item label="Record Time"><el-date-picker v-model="form.recordTime" type="datetime"/></el-form-item>
<el-button type="primary" @click="create">Save</el-button>
</el-form>
<div style="margin-top: 16px;">
<el-input v-model="filterElderId" placeholder="Elder ID" style="width: 200px;" />
<el-button @click="load" style="margin-left: 8px;">Search</el-button>
<el-table :data="items" stripe style="margin-top: 12px;">
<el-table-column prop="elderId" label="Elder ID" width="100" />
<el-table-column prop="temperature" label="Temp" />
<el-table-column prop="bpSystolic" label="BP S" />
<el-table-column prop="bpDiastolic" label="BP D" />
<el-table-column prop="heartRate" label="HR" />
<el-table-column prop="recordTime" label="Record Time" />
</el-table>
</div>
</div>
</template>
<script>
import { nurseHealthCreate, nurseHealthList } from "../../api";
import { formatDateTime } from "../../utils/date";
export default {
data() {
return {
form: { elderId: "", temperature: "", bpSystolic: "", bpDiastolic: "", heartRate: "", note: "", recordTime: "" },
filterElderId: "",
items: []
};
},
methods: {
async create() {
try {
const payload = { ...this.form, recordTime: formatDateTime(this.form.recordTime) };
await nurseHealthCreate(payload);
this.$message.success("saved");
} catch (e) {
this.$message.error(e.message || "save failed");
}
},
async load() {
if (!this.filterElderId) return;
try {
const res = await nurseHealthList(this.filterElderId);
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
}
};
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="page-card">
<h3>Notices</h3>
<el-table :data="items" stripe>
<el-table-column prop="title" label="Title" />
<el-table-column prop="content" label="Content" />
<el-table-column prop="createdAt" label="Created" />
</el-table>
</div>
</template>
<script>
import { nurseNoticeList } from "../../api";
export default {
data() {
return { items: [] };
},
async created() {
try {
const res = await nurseNoticeList();
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
};
</script>

View File

@@ -0,0 +1,36 @@
<template>
<div class="page-card">
<h3>My Schedule</h3>
<div style="margin-bottom: 12px; display:flex; gap: 12px;">
<el-date-picker v-model="date" type="date" placeholder="Pick date" />
<el-button type="primary" @click="load">Search</el-button>
</div>
<el-table :data="items" stripe>
<el-table-column prop="date" label="Date" />
<el-table-column prop="shift" label="Shift" />
<el-table-column prop="task" label="Task" />
</el-table>
</div>
</template>
<script>
import { nurseSchedules } from "../../api";
import { formatDate } from "../../utils/date";
export default {
data() {
return { date: "", items: [] };
},
methods: {
async load() {
if (!this.date) return;
try {
const res = await nurseSchedules(formatDate(this.date));
this.items = res.data.data;
} catch (e) {
this.$message.error(e.message || "load failed");
}
}
}
};
</script>