更新若干事项

This commit is contained in:
hero920103 committed 2026-10-26 08:54:34 +08:00
1 parent 68cdaf38fa
commit 9898962836
30 files changed
+7715

No files matched your search

+1
View File
@@ -28,6 +28,7 @@ declare module 'vue' {
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+51
View File
@@ -0,0 +1,51 @@
import request from "@/utils/request";
// 平台端排班模板管理(全局模板,只有平台可维护)
// 系统预置模板只读:后端会拒绝对 is_system=1 的编辑 / 删除 / 停用
const PREFIX = "/platform/schedTemplates";
export function getSchedTemplateList(params) {
return request({
url: PREFIX,
method: "get",
params,
});
}
export function getSchedTemplateDetail(id) {
return request({
url: `${PREFIX}/${id}`,
method: "get",
});
}
export function createSchedTemplate(data) {
return request({
url: PREFIX,
method: "post",
data,
});
}
export function updateSchedTemplate(id, data) {
return request({
url: `${PREFIX}/${id}`,
method: "post",
data,
});
}
export function deleteSchedTemplate(id) {
return request({
url: `${PREFIX}/${id}`,
method: "delete",
});
}
export function toggleSchedTemplateStatus(id, status) {
return request({
url: `${PREFIX}/${id}/status`,
method: "post",
data: { status },
});
}
+7
View File
@@ -73,6 +73,13 @@ const staticMainChildren = [
component: () => import("@/views/template/tags.vue"),
meta: { requiresAuth: true, title: "标签调用说明" }
},
// 排班模板(全局模板管理,只有平台可维护)
{
path: "/basicSettings/schedTemplate",
name: "PlatformSchedTemplate",
component: () => import("@/views/system/schedTemplate/index.vue"),
meta: { requiresAuth: true, title: "排班模板" }
},
// 兼容拼写错误的路径重定向
{
path: "/apps/erp/dashborad",
@@ -0,0 +1,523 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="960px"
top="6vh"
destroy-on-close
@closed="handleClosed"
>
<div v-loading="loading">
<el-form :model="form" label-width="110px" :disabled="readonly">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="模板名称" required>
<el-input v-model="form.name" maxlength="50" placeholder="如:项目部大小周" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模板类型">
<el-select v-model="form.template_type" style="width: 100%">
<el-option
v-for="item in typeOptions"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排班方式" required>
<el-radio-group v-model="form.align_mode">
<el-radio :value="1">按自然周</el-radio>
<el-radio :value="2">按开始日滚动</el-radio>
<el-radio :value="3">按自然月</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序">
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" />
<span class="tip">数字越小越靠前</span>
</el-form-item>
</el-col>
<el-col v-if="form.align_mode !== 3" :span="12">
<el-form-item label="周期天数" required>
<el-select v-if="form.align_mode === 1" v-model="form.cycle_days" style="width: 100%">
<el-option :value="7" label="1 周(7 天)" />
<el-option :value="14" label="2 周(14 天,大小周常用)" />
<el-option :value="21" label="3 周(21 天)" />
<el-option :value="28" label="4 周(28 天)" />
</el-select>
<el-input-number
v-else
v-model="form.cycle_days"
:min="2"
:max="31"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col v-if="form.align_mode === 1" :span="12">
<el-form-item label="起始周">
<el-select v-model="form.week_offset" style="width: 100%">
<el-option :value="0" label="方案开始周按第 1 周算" />
<el-option v-if="form.cycle_days > 7" :value="1" label="方案开始周按第 2 周算" />
</el-select>
</el-form-item>
</el-col>
<el-col v-if="form.align_mode === 3" :span="12">
<el-form-item label="每月休息天数" required>
<el-input-number
v-model="form.month_rest_days"
:min="1"
:max="15"
controls-position="right"
/>
<span class="tip">天</span>
</el-form-item>
</el-col>
<el-col v-if="form.align_mode === 3" :span="12">
<el-form-item label="休息日安排">
<el-select v-model="form.month_rest_weekday" style="width: 100%">
<el-option :value="0" label="不固定(从月末往前排)" />
<el-option v-for="item in weekdayOptions" :key="item.value" :value="item.value" :label="`优先休${item.label}`" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="规则摘要">
<el-input
v-model="form.summary"
maxlength="255"
placeholder="留空由系统自动生成,用于租户端列表展示"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="rules-head">
<div class="label">
{{ form.align_mode === 3 ? "上班日规则" : "周期每日规则" }}
</div>
<div class="hint" v-if="form.align_mode === 1">
序号按自然周固定:周一 → 周日;多周循环时第 2 周从第 8 天开始
</div>
<div class="hint" v-else-if="form.align_mode === 2">
从方案开始日期起,按顺序循环使用下面 {{ form.cycle_days }} 天的规则
</div>
<div class="hint" v-else>
除每月固定休息日外,其余日期都按下面这一天的班次上班
</div>
</div>
<el-table :data="form.days" border size="small" :disabled="readonly">
<el-table-column label="天数" width="120">
<template #default="{ row, $index }">{{ dayLabel($index, row) }}</template>
</el-table-column>
<el-table-column label="类型" width="130">
<template #default="{ row }">
<el-radio-group v-model="row.day_type" size="small" :disabled="readonly">
<el-radio-button :value="1">上班</el-radio-button>
<el-radio-button :value="2">休息</el-radio-button>
</el-radio-group>
</template>
</el-table-column>
<el-table-column label="班次名称" width="130">
<template #default="{ row }">
<el-input
v-model="row.shift_name"
size="small"
maxlength="50"
:disabled="readonly || row.day_type === 2"
placeholder="如:白班"
/>
</template>
</el-table-column>
<el-table-column label="简称" width="80">
<template #default="{ row }">
<el-input
v-model="row.shift_short"
size="small"
maxlength="20"
:disabled="readonly || row.day_type === 2"
placeholder="白"
/>
</template>
</el-table-column>
<el-table-column label="上班时间" width="100">
<template #default="{ row }">
<el-input
v-model="row.work_start"
size="small"
placeholder="09:00"
:disabled="readonly || row.day_type === 2"
/>
</template>
</el-table-column>
<el-table-column label="下班时间" width="100">
<template #default="{ row }">
<el-input
v-model="row.work_end"
size="small"
placeholder="18:00"
:disabled="readonly || row.day_type === 2"
/>
</template>
</el-table-column>
<el-table-column label="休息开始" width="100">
<template #default="{ row }">
<el-input
v-model="row.rest_start"
size="small"
placeholder="12:00"
:disabled="readonly || row.day_type === 2"
/>
</template>
</el-table-column>
<el-table-column label="休息结束" width="100">
<template #default="{ row }">
<el-input
v-model="row.rest_end"
size="small"
placeholder="13:30"
:disabled="readonly || row.day_type === 2"
/>
</template>
</el-table-column>
<el-table-column label="颜色" width="70" align="center">
<template #default="{ row }">
<el-color-picker
v-model="row.color"
size="small"
:disabled="readonly || row.day_type === 2"
/>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="visible = false">{{ readonly ? "关闭" : "取消" }}</el-button>
<el-button v-if="!readonly" type="primary" :loading="submitting" @click="submit">
保存
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch, nextTick } from "vue";
import {
getSchedTemplateDetail,
createSchedTemplate,
updateSchedTemplate,
} from "@/api/schedTemplate";
const props = defineProps({
modelValue: { type: Boolean, default: false },
templateId: { type: Number, default: 0 },
readonly: { type: Boolean, default: false },
});
const emit = defineEmits(["update:modelValue", "saved"]);
const visible = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
const typeOptions = [
{ value: 1, label: "双休" },
{ value: 2, label: "单休" },
{ value: 3, label: "大小周" },
{ value: 4, label: "月休" },
{ value: 5, label: "三班倒" },
{ value: 6, label: "做一休一" },
{ value: 9, label: "自定义" },
];
const weekdayOptions = [
{ value: 1, label: "周一" },
{ value: 2, label: "周二" },
{ value: 3, label: "周三" },
{ value: 4, label: "周四" },
{ value: 5, label: "周五" },
{ value: 6, label: "周六" },
{ value: 7, label: "周日" },
];
const weekdayNames = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const loading = ref(false);
const submitting = ref(false);
// 回填详情时避免 watch 把已加载的规则重新重建
let skipSync = false;
function blankDay(index) {
return {
day_index: index,
day_type: 1,
shift_name: "白班",
shift_short: "白",
color: "#409EFF",
work_start: "09:00",
work_end: "18:00",
rest_start: "12:00",
rest_end: "13:30",
work_hours: 7.5,
remark: "",
};
}
function restDay(index) {
return {
day_index: index,
day_type: 2,
shift_name: "",
shift_short: "",
color: "",
work_start: "",
work_end: "",
rest_start: "",
rest_end: "",
work_hours: 0,
remark: "",
};
}
function defaultForm() {
const days = [];
for (let i = 0; i < 7; i += 1) {
days.push(i >= 5 ? restDay(i) : blankDay(i));
}
return {
id: 0,
name: "",
code: "",
template_type: 9,
align_mode: 1,
cycle_days: 7,
week_offset: 0,
month_rest_days: 4,
month_rest_weekday: 7,
summary: "",
status: 1,
sort: 0,
days,
};
}
const form = reactive(defaultForm());
const dialogTitle = computed(() => {
if (props.readonly) return "查看排班模板";
return props.templateId ? "编辑排班模板" : "新建排班模板";
});
function dayLabel(index, row) {
if (form.align_mode === 3) return "上班日";
if (form.align_mode === 2) return `第 ${index + 1} 天`;
const week = Math.floor(index / 7) + 1;
const name = weekdayNames[index % 7] || row.day_index;
return form.cycle_days > 7 ? `第 ${week} 周 ${name}` : name;
}
// 周期天数或排班方式变化时,保留已填规则并补齐/裁剪天数
function syncDays() {
const cycle = form.align_mode === 3 ? 1 : Number(form.cycle_days) || 7;
const next = [];
for (let i = 0; i < cycle; i += 1) {
const current = form.days[i];
if (current) {
next.push({ ...current, day_index: i });
} else {
next.push(blankDay(i));
}
}
form.days = next;
}
watch(
() => form.cycle_days,
() => {
if (!props.readonly && !skipSync) syncDays();
}
);
watch(
() => form.align_mode,
(value) => {
if (props.readonly || skipSync) return;
if (value === 3) {
form.month_rest_days = form.month_rest_days || 4;
} else if (value === 1 && form.cycle_days % 7 !== 0) {
form.cycle_days = 7;
}
syncDays();
}
);
// 月休的休息日安排由后端按月推算,这里只要保证至少有一天的上班规则
watch(
() => form.modelValue,
(open) => {
if (open) loadDetail();
}
);
async function loadDetail() {
if (!props.templateId) {
skipSync = true;
Object.assign(form, defaultForm());
await nextTick();
skipSync = false;
return;
}
loading.value = true;
try {
const res = await getSchedTemplateDetail(props.templateId);
const data = res?.data || {};
skipSync = true;
Object.assign(form, {
id: data.id || 0,
name: data.name || "",
code: data.code || "",
template_type: data.template_type || 9,
align_mode: data.align_mode || 1,
cycle_days: data.cycle_days || 7,
week_offset: data.week_offset || 0,
month_rest_days: data.month_rest_days || 0,
month_rest_weekday: data.month_rest_weekday || 0,
summary: data.summary || "",
status: data.status === 0 ? 0 : 1,
sort: data.sort || 0,
});
form.days = (data.days || []).map((item) => ({
day_index: item.day_index,
day_type: item.day_type,
shift_name: item.shift_name || "",
shift_short: item.shift_short || "",
color: item.color || "#409EFF",
work_start: item.work_start || "",
work_end: item.work_end || "",
rest_start: item.rest_start || "",
rest_end: item.rest_end || "",
work_hours: item.work_hours || 0,
remark: item.remark || "",
}));
if (form.days.length === 0) syncDays();
await nextTick();
skipSync = false;
} catch (e) {
skipSync = false;
ElMessage.error(e?.message || "加载模板详情失败");
} finally {
loading.value = false;
}
}
function validate() {
if (!form.name.trim()) return "请填写模板名称";
if (form.align_mode !== 3) {
if (!form.cycle_days || form.cycle_days < 2) return "周期天数至少 2 天";
if (form.align_mode === 1 && form.cycle_days % 7 !== 0) {
return "按自然周排班时,周期天数必须是 7 的倍数";
}
if (form.days.length !== form.cycle_days) return "每日规则数量与周期天数不一致";
} else if (!form.month_rest_days || form.month_rest_days < 1) {
return "请填写每月休息天数";
}
const workDays = form.days.filter((item) => item.day_type === 1);
if (workDays.length === 0) return "至少需要一天上班";
for (const item of workDays) {
if (!String(item.shift_name || "").trim()) return "上班日需要填写班次名称";
if (!/^\d{1,2}:\d{2}$/.test(item.work_start) || !/^\d{1,2}:\d{2}$/.test(item.work_end)) {
return "上下班时间格式应为 HH:mm";
}
}
return "";
}
async function submit() {
const message = validate();
if (message) {
ElMessage.warning(message);
return;
}
const payload = {
name: form.name.trim(),
code: form.code || "",
template_type: form.template_type,
align_mode: form.align_mode,
cycle_days: form.align_mode === 3 ? 0 : form.cycle_days,
week_offset: form.week_offset,
month_rest_days: form.align_mode === 3 ? form.month_rest_days : 0,
month_rest_weekday: form.align_mode === 3 ? form.month_rest_weekday : 0,
summary: form.summary,
status: form.status,
sort: form.sort,
days: form.days.map((item, index) => ({
day_index: index,
day_type: item.day_type,
shift_name: item.day_type === 1 ? item.shift_name : "",
shift_short: item.day_type === 1 ? item.shift_short : "",
color: item.day_type === 1 ? item.color || "#409EFF" : "",
work_start: item.day_type === 1 ? item.work_start : "",
work_end: item.day_type === 1 ? item.work_end : "",
rest_start: item.day_type === 1 ? item.rest_start : "",
rest_end: item.day_type === 1 ? item.rest_end : "",
work_hours: item.day_type === 1 ? item.work_hours : 0,
remark: item.remark || "",
})),
};
submitting.value = true;
try {
if (props.templateId) {
await updateSchedTemplate(props.templateId, payload);
ElMessage.success("模板已更新");
} else {
await createSchedTemplate(payload);
ElMessage.success("模板已创建");
}
visible.value = false;
emit("saved");
} catch (e) {
ElMessage.error(e?.message || "保存失败");
} finally {
submitting.value = false;
}
}
function handleClosed() {
skipSync = true;
Object.assign(form, defaultForm());
nextTick(() => {
skipSync = false;
});
}
</script>
<style scoped>
.rules-head {
display: flex;
align-items: baseline;
gap: 12px;
margin: 4px 0 8px;
}
.rules-head .label {
font-size: 14px;
font-weight: 600;
}
.rules-head .hint {
color: #909399;
font-size: 12px;
}
.tip {
margin-left: 8px;
color: #909399;
font-size: 12px;
}
</style>
@@ -0,0 +1,212 @@
<template>
<div class="sched-template-page">
<div class="page-header">
<div class="title">排班模板</div>
<div class="actions">
<el-input
v-model="keyword"
placeholder="搜索模板名称"
clearable
style="width: 220px"
@keyup.enter="loadList"
@clear="loadList"
/>
<el-button @click="loadList">查询</el-button>
<el-button type="primary" @click="openCreate">新建模板</el-button>
</div>
</div>
<el-alert
type="info"
:closable="false"
show-icon
title="系统预置模板全局生效、只读不可改;租户只能调用,调用后规则会复制到租户自己的方案里,可自由调整"
style="margin-bottom: 12px"
/>
<el-table :data="list" v-loading="loading" border>
<el-table-column prop="sort" label="排序" width="70" align="center" />
<el-table-column label="模板名称" min-width="150">
<template #default="{ row }">
<span>{{ row.name }}</span>
<el-tag v-if="row.is_system === 1" size="small" type="warning" style="margin-left: 6px">
预置
</el-tag>
</template>
</el-table-column>
<el-table-column prop="template_type_text" label="类型" width="100" />
<el-table-column label="排班方式" width="190">
<template #default="{ row }">{{ alignText(row) }}</template>
</el-table-column>
<el-table-column prop="summary" label="规则摘要" min-width="280" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">
{{ row.status === 1 ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="creater" label="创建人" width="100" />
<el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openView(row)">查看</el-button>
<el-button v-if="row.editable" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button
v-if="row.editable"
link
:type="row.status === 1 ? 'warning' : 'success'"
@click="toggleStatus(row)"
>
{{ row.status === 1 ? "停用" : "启用" }}
</el-button>
<el-button v-if="row.editable" link type="danger" @click="removeRow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
background
@current-change="loadList"
@size-change="loadList"
/>
</div>
<TemplateEdit
v-model="dialogVisible"
:template-id="currentId"
:readonly="readonly"
@saved="loadList"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import TemplateEdit from "./components/TemplateEdit.vue";
import {
getSchedTemplateList,
deleteSchedTemplate,
toggleSchedTemplateStatus,
} from "@/api/schedTemplate";
const keyword = ref("");
const list = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const loading = ref(false);
const dialogVisible = ref(false);
const currentId = ref(0);
const readonly = ref(false);
function alignText(row) {
if (row.align_mode === 3) {
return `按自然月 · 每月休 ${row.month_rest_days || 0} 天`;
}
if (row.align_mode === 2) {
return `每 ${row.cycle_days || 0} 天滚动循环`;
}
const weeks = Math.max(1, Math.round((row.cycle_days || 7) / 7));
return weeks > 1 ? `按自然周 · ${weeks} 周循环` : "按自然周";
}
async function loadList() {
loading.value = true;
try {
const res = await getSchedTemplateList({
page: page.value,
pageSize: pageSize.value,
keyword: keyword.value,
});
const data = res?.data || {};
list.value = data.list || [];
total.value = data.total || 0;
} catch (e) {
ElMessage.error(e?.message || "加载排班模板失败");
} finally {
loading.value = false;
}
}
function openCreate() {
currentId.value = 0;
readonly.value = false;
dialogVisible.value = true;
}
function openEdit(row) {
currentId.value = row.id;
readonly.value = false;
dialogVisible.value = true;
}
function openView(row) {
currentId.value = row.id;
readonly.value = true;
dialogVisible.value = true;
}
async function toggleStatus(row) {
const next = row.status === 1 ? 0 : 1;
try {
await toggleSchedTemplateStatus(row.id, next);
ElMessage.success(next === 1 ? "模板已启用" : "模板已停用");
loadList();
} catch (e) {
ElMessage.error(e?.message || "更新状态失败");
}
}
async function removeRow(row) {
try {
await ElMessageBox.confirm(
`确认删除模板「${row.name}」?删除后租户将无法再调用该模板,已生成的方案不受影响。`,
"删除确认",
{ type: "warning" }
);
} catch (e) {
return;
}
try {
await deleteSchedTemplate(row.id);
ElMessage.success("模板已删除");
loadList();
} catch (e) {
ElMessage.error(e?.message || "删除失败");
}
}
onMounted(loadList);
</script>
<style scoped>
.sched-template-page {
padding: 16px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.page-header .title {
font-size: 18px;
font-weight: 600;
}
.page-header .actions {
display: flex;
gap: 8px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
</style>