This commit is contained in:
王子琦
2026-01-20 13:56:37 +08:00
parent 19249fb374
commit 438eb0b635
7 changed files with 353 additions and 82 deletions

View File

@@ -162,6 +162,14 @@ public class AdminController {
return ApiResponse.success(scheduleMapper.listByDate(java.time.LocalDate.parse(date)));
}
@GetMapping("/schedules/range")
public ApiResponse<List<Schedule>> listSchedulesRange(@RequestParam String start, @RequestParam String end) {
return ApiResponse.success(scheduleMapper.listByDateRange(
java.time.LocalDate.parse(start),
java.time.LocalDate.parse(end)
));
}
@PostMapping("/schedules")
public ApiResponse<Schedule> createSchedule(@RequestBody ScheduleRequest request) {
Schedule schedule = new Schedule();

View File

@@ -40,6 +40,16 @@ public class NurseController {
return ApiResponse.success(scheduleMapper.listByNurseAndDate(nurseId, java.time.LocalDate.parse(date)));
}
@GetMapping("/schedules/range")
public ApiResponse<List<Schedule>> listSchedulesRange(@RequestParam String start, @RequestParam String end) {
Long nurseId = Long.valueOf(StpUtil.getLoginId().toString());
return ApiResponse.success(scheduleMapper.listByNurseAndDateRange(
nurseId,
java.time.LocalDate.parse(start),
java.time.LocalDate.parse(end)
));
}
@PostMapping("/care-records")
public ApiResponse<CareRecord> createCare(@RequestBody CareRecordRequest request) {
CareRecord record = new CareRecord();

View File

@@ -14,6 +14,12 @@ public interface ScheduleMapper {
@Select("SELECT * FROM schedule WHERE date = #{date} ORDER BY id DESC")
List<Schedule> listByDate(LocalDate date);
@Select("SELECT * FROM schedule WHERE date BETWEEN #{start} AND #{end} ORDER BY date ASC")
List<Schedule> listByDateRange(@Param("start") LocalDate start, @Param("end") LocalDate end);
@Select("SELECT * FROM schedule WHERE nurse_id = #{nurseId} AND date BETWEEN #{start} AND #{end} ORDER BY date ASC")
List<Schedule> listByNurseAndDateRange(@Param("nurseId") Long nurseId, @Param("start") LocalDate start, @Param("end") LocalDate end);
@Insert("INSERT INTO schedule(nurse_id, date, shift, task, created_at, updated_at) VALUES(#{nurseId}, #{date}, #{shift}, #{task}, NOW(), NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Schedule schedule);

View File

@@ -17,6 +17,7 @@ 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 schedulesByRange = (start, end) => http.get("/admin/schedules/range", { params: { start, end } });
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}`);
@@ -31,6 +32,7 @@ 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 nurseSchedulesRange = (start, end) => http.get("/nurse/schedules/range", { params: { start, end } });
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);

View File

@@ -1,4 +1,4 @@
import Vue from "vue";
import Vue from "vue";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import "./assets/theme.css";

View File

@@ -1,108 +1,244 @@
<template>
<template>
<div class="page-card">
<h3>排班管理</h3>
<div style="margin: 12px 0; display:flex; gap: 12px;">
<el-date-picker v-model="date" type="date" placeholder="选择日期" />
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="showCreate = true">新增排班</el-button>
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>排班管理</h3>
<el-button type="primary" @click="openCreate">新增排班</el-button>
</div>
<el-table :data="schedules" stripe>
<el-table-column prop="id" label="编号" width="80" />
<el-table-column prop="nurseId" label="护工ID" width="100" />
<el-table-column prop="date" label="日期" />
<el-table-column prop="shift" label="班次" />
<el-table-column prop="task" label="任务" />
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="edit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="schedule-toolbar">
<el-date-picker v-model="monthPicker" type="month" placeholder="选择月份" @change="handleMonthChange" />
<el-button @click="jumpToday">回到今天</el-button>
</div>
<el-row :gutter="16">
<el-col :span="16">
<el-calendar v-model="calendarDate" @input="handleDateChange">
<template slot="dateCell" slot-scope="{ data }">
<div class="calendar-cell">
<div class="cell-date">{{ data.day.split('-')[2] }}</div>
<div class="cell-badges">
<span v-for="item in getDaySchedules(data.day).slice(0, 2)" :key="item.id" class="badge">
{{ formatBadge(item) }}
</span>
<span v-if="getDaySchedules(data.day).length > 2" class="more">
+{{ getDaySchedules(data.day).length - 2 }}
</span>
</div>
</div>
</template>
</el-calendar>
</el-col>
<el-col :span="8">
<div class="schedule-panel">
<div class="schedule-title">当天排班{{ selectedDate }}</div>
<el-table :data="daySchedules" stripe>
<el-table-column label="护工" width="120">
<template slot-scope="scope">
{{ nurseName(scope.row.nurseId) }}
</template>
</el-table-column>
<el-table-column prop="shift" label="班次" width="90" />
<el-table-column prop="task" label="任务" />
<el-table-column label="操作" width="160" class-name="action-col">
<template slot-scope="scope">
<div class="action-inline">
<el-button size="mini" @click="openEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="remove(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
<el-dialog title="新增排班" :visible.sync="showCreate">
<el-dialog title="排班信息" :visible.sync="showDialog">
<el-form :model="form" label-width="120px">
<el-form-item label="护工ID"><el-input v-model="form.nurseId"/></el-form-item>
<el-form-item label="护工">
<el-select v-model="form.nurseId" placeholder="选择护工">
<el-option v-for="nurse in nurses" :key="nurse.id" :label="nurse.name" :value="nurse.id" />
</el-select>
</el-form-item>
<el-form-item label="日期"><el-date-picker v-model="form.date" type="date"/></el-form-item>
<el-form-item label="班次"><el-input v-model="form.shift"/></el-form-item>
<el-form-item label="班次">
<el-select v-model="form.shift" placeholder="选择班次">
<el-option label="早班" value="早班" />
<el-option label="中班" value="中班" />
<el-option label="晚班" value="晚班" />
<el-option label="夜班" value="夜班" />
</el-select>
</el-form-item>
<el-form-item label="任务"><el-input v-model="form.task"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showCreate = false">取消</el-button>
<el-button type="primary" @click="create">保存</el-button>
</span>
</el-dialog>
<el-dialog title="编辑排班" :visible.sync="showEdit">
<el-form :model="editForm" label-width="120px">
<el-form-item label="护工ID"><el-input v-model="editForm.nurseId"/></el-form-item>
<el-form-item label="日期"><el-date-picker v-model="editForm.date" type="date"/></el-form-item>
<el-form-item label="班次"><el-input v-model="editForm.shift"/></el-form-item>
<el-form-item label="任务"><el-input v-model="editForm.task"/></el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showEdit = false">取消</el-button>
<el-button type="primary" @click="update">保存</el-button>
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { schedulesByDate, scheduleCreate, scheduleUpdate, scheduleDelete } from "../../api";
import { schedulesByRange, scheduleCreate, scheduleUpdate, scheduleDelete, adminUsers } from "../../api";
import { formatDate } from "../../utils/date";
export default {
data() {
const today = new Date();
return {
date: "",
schedules: [],
showCreate: false,
showEdit: false,
form: { nurseId: "", date: "", shift: "", task: "" },
editForm: {}
calendarDate: today,
monthPicker: today,
selectedDate: formatDate(today),
daySchedules: [],
scheduleMap: {},
nurses: [],
nurseMap: {},
showDialog: false,
form: { id: null, nurseId: "", date: "", shift: "", task: "" }
};
},
created() {
this.loadMonth(this.monthPicker);
this.loadNurses();
},
methods: {
async load() {
if (!this.date) return;
async loadNurses() {
try {
const res = await schedulesByDate(formatDate(this.date));
this.schedules = res.data.data;
const res = await adminUsers("NURSE");
this.nurses = res.data.data;
const map = {};
this.nurses.forEach((nurse) => {
map[nurse.id] = nurse.name;
});
this.nurseMap = map;
} catch (e) {
this.$message.error(e.message || "load failed");
this.$message.error(e.message || "加载护工失败");
}
},
async create() {
nurseName(id) {
return this.nurseMap[id] || `护工${id}`;
},
formatBadge(item) {
const shift = item.shift || "班次";
const name = this.nurseName(item.nurseId);
return `${shift}·${name}`;
},
handleMonthChange() {
if (!this.monthPicker) return;
const firstDay = new Date(this.monthPicker.getFullYear(), this.monthPicker.getMonth(), 1);
this.calendarDate = firstDay;
this.loadMonth(this.monthPicker);
},
handleDateChange(date) {
this.selectedDate = formatDate(date);
this.daySchedules = this.getDaySchedules(this.selectedDate);
},
getDaySchedules(dayStr) {
return this.scheduleMap[dayStr] || [];
},
async loadMonth(date) {
const year = date.getFullYear();
const month = date.getMonth();
const start = new Date(year, month, 1);
const end = new Date(year, month + 1, 0);
try {
await scheduleCreate({ ...this.form, date: formatDate(this.form.date) });
this.showCreate = false;
this.load();
const res = await schedulesByRange(formatDate(start), formatDate(end));
const map = {};
res.data.data.forEach((item) => {
if (!map[item.date]) {
map[item.date] = [];
}
map[item.date].push(item);
});
this.scheduleMap = map;
this.daySchedules = this.getDaySchedules(this.selectedDate);
} catch (e) {
this.$message.error(e.message || "create failed");
this.$message.error(e.message || "加载失败");
}
},
edit(row) {
this.editForm = { ...row };
this.showEdit = true;
openCreate() {
this.form = { id: null, nurseId: "", date: new Date(this.selectedDate), shift: "", task: "" };
this.showDialog = true;
},
async update() {
openEdit(row) {
this.form = { ...row, date: new Date(row.date) };
this.showDialog = true;
},
async save() {
try {
await scheduleUpdate({ ...this.editForm, date: formatDate(this.editForm.date) });
this.showEdit = false;
this.load();
const payload = { ...this.form, date: formatDate(this.form.date) };
if (payload.id) {
await scheduleUpdate(payload);
} else {
await scheduleCreate(payload);
}
this.showDialog = false;
this.loadMonth(this.monthPicker);
} catch (e) {
this.$message.error(e.message || "update failed");
this.$message.error(e.message || "保存失败");
}
},
async remove(row) {
try {
await scheduleDelete(row.id);
this.load();
this.loadMonth(this.monthPicker);
} catch (e) {
this.$message.error(e.message || "delete failed");
this.$message.error(e.message || "删除失败");
}
},
jumpToday() {
const today = new Date();
this.monthPicker = today;
this.calendarDate = today;
this.selectedDate = formatDate(today);
this.loadMonth(today);
}
}
};
</script>
<style scoped>
.schedule-toolbar {
margin: 12px 0 16px;
display: flex;
gap: 12px;
align-items: center;
}
.schedule-panel {
background: #f6fbf7;
border-radius: 8px;
padding: 12px;
border: 1px solid #d9efe3;
}
.schedule-title {
font-weight: 600;
margin-bottom: 10px;
}
.action-inline {
display: flex;
gap: 6px;
flex-wrap: nowrap;
}
.calendar-cell {
min-height: 58px;
}
.cell-date {
font-size: 12px;
color: #6b7b6f;
}
.cell-badges {
margin-top: 4px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.badge {
background: #e1f3ea;
color: #2f7d59;
padding: 2px 6px;
border-radius: 6px;
font-size: 11px;
}
.more {
color: #7b8b7f;
font-size: 11px;
}
</style>

View File

@@ -1,36 +1,145 @@
<template>
<template>
<div class="page-card">
<h3>我的排班</h3>
<div style="margin-bottom: 12px; display:flex; gap: 12px;">
<el-date-picker v-model="date" type="date" placeholder="选择日期" />
<el-button type="primary" @click="load">查询</el-button>
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>我的排班</h3>
<el-button @click="jumpToday">回到今天</el-button>
</div>
<el-table :data="items" stripe>
<el-table-column prop="date" label="日期" />
<el-table-column prop="shift" label="班次" />
<el-table-column prop="task" label="任务" />
</el-table>
<div class="schedule-toolbar">
<el-date-picker v-model="monthPicker" type="month" placeholder="选择月份" @change="handleMonthChange" />
</div>
<el-row :gutter="16">
<el-col :span="16">
<el-calendar v-model="calendarDate" @input="handleDateChange">
<template slot="dateCell" slot-scope="{ data }">
<div class="calendar-cell">
<div class="cell-date">{{ data.day.split('-')[2] }}</div>
<div class="cell-badges">
<span v-for="item in getDaySchedules(data.day).slice(0, 2)" :key="item.id" class="badge">
{{ item.shift || "班次" }}
</span>
<span v-if="getDaySchedules(data.day).length > 2" class="more">
+{{ getDaySchedules(data.day).length - 2 }}
</span>
</div>
</div>
</template>
</el-calendar>
</el-col>
<el-col :span="8">
<div class="schedule-panel">
<div class="schedule-title">当天排班{{ selectedDate }}</div>
<el-table :data="daySchedules" stripe>
<el-table-column prop="shift" label="班次" width="100" />
<el-table-column prop="task" label="任务" />
</el-table>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { nurseSchedules } from "../../api";
import { nurseSchedulesRange } from "../../api";
import { formatDate } from "../../utils/date";
export default {
data() {
return { date: "", items: [] };
const today = new Date();
return {
calendarDate: today,
monthPicker: today,
selectedDate: formatDate(today),
daySchedules: [],
scheduleMap: {}
};
},
created() {
this.loadMonth(this.monthPicker);
},
methods: {
async load() {
if (!this.date) return;
handleMonthChange() {
if (!this.monthPicker) return;
const firstDay = new Date(this.monthPicker.getFullYear(), this.monthPicker.getMonth(), 1);
this.calendarDate = firstDay;
this.loadMonth(this.monthPicker);
},
handleDateChange(date) {
this.selectedDate = formatDate(date);
this.daySchedules = this.getDaySchedules(this.selectedDate);
},
getDaySchedules(dayStr) {
return this.scheduleMap[dayStr] || [];
},
async loadMonth(date) {
const year = date.getFullYear();
const month = date.getMonth();
const start = new Date(year, month, 1);
const end = new Date(year, month + 1, 0);
try {
const res = await nurseSchedules(formatDate(this.date));
this.items = res.data.data;
const res = await nurseSchedulesRange(formatDate(start), formatDate(end));
const map = {};
res.data.data.forEach((item) => {
if (!map[item.date]) {
map[item.date] = [];
}
map[item.date].push(item);
});
this.scheduleMap = map;
this.daySchedules = this.getDaySchedules(this.selectedDate);
} catch (e) {
this.$message.error(e.message || "load failed");
this.$message.error(e.message || "加载失败");
}
},
jumpToday() {
const today = new Date();
this.monthPicker = today;
this.calendarDate = today;
this.selectedDate = formatDate(today);
this.loadMonth(today);
}
}
};
</script>
<style scoped>
.schedule-toolbar {
margin: 12px 0 16px;
display: flex;
gap: 12px;
align-items: center;
}
.schedule-panel {
background: #f6fbf7;
border-radius: 8px;
padding: 12px;
border: 1px solid #d9efe3;
}
.schedule-title {
font-weight: 600;
margin-bottom: 10px;
}
.calendar-cell {
min-height: 58px;
}
.cell-date {
font-size: 12px;
color: #6b7b6f;
}
.cell-badges {
margin-top: 4px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.badge {
background: #e1f3ea;
color: #2f7d59;
padding: 2px 6px;
border-radius: 6px;
font-size: 11px;
}
.more {
color: #7b8b7f;
font-size: 11px;
}
</style>