Merge remote-tracking branch 'origin/main' into main

# Conflicts:
#	src/views/planMange/monthlyPlan/index.vue
This commit is contained in:
马三炮 2025-12-26 18:46:40 +08:00
commit 15758ee0b7
35 changed files with 3384 additions and 607 deletions

View File

@ -6,3 +6,6 @@ VITE_APP_ENV = 'development'
# 运检站管理/开发环境
VITE_APP_BASE_API = '/dev-api'
#静态资源路径(服务器目录)
VITE_APP_PUBLIC_PATH = ''

View File

@ -5,7 +5,10 @@ VITE_APP_TITLE = 运检站管理
VITE_APP_ENV = 'production'
# 运检站管理/生产环境
VITE_APP_BASE_API = '/prod-api'
VITE_APP_BASE_API = '/ynDigitalGadgets-prod-api'
#静态资源路径(服务器目录)
VITE_APP_PUBLIC_PATH = '/ynDigitalGadgets'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -1,36 +1,45 @@
import request from '@/utils/request'
// 计划管理- 查询列表
export function listPlanAPI(query) {
// 计划管理- 查询列表
export function listDailyPlanAPI(query) {
return request({
url: '/personnel/getPersonnelList',
url: '/dayPlan/getDayPlanList',
method: 'GET',
params: query,
})
}
// 计划管理- 新增
export function addPlanAPI(data) {
// 计划管理- 新增
export function addDailyPlanAPI(data) {
return request({
url: '/personnel/addPersonnel',
url: '/dayPlan/addDayPlan',
method: 'POST',
data,
})
}
// 计划管理- 修改
export function updatePlanAPI(data) {
// 日计划管理- 新增弹框内获取的计划列表
export function listDailyPlanPopupAPI(query) {
return request({
url: '/personnel/updatePersonnel',
url: '/monthlyPlan/getMonthlyPlanLists',
method: 'GET',
params: query,
})
}
// 日计划管理- 修改
export function updateDailyPlanAPI(data) {
return request({
url: '/dayPlan/updateDayPlan',
method: 'POST',
data,
})
}
// 计划管理- 删除
export function delPlanAPI(data) {
// 计划管理- 删除
export function delDailyPlanAPI(data) {
return request({
url: `/personnel/delPersonnel`,
url: `/dayPlan/delDayPlan`,
method: 'POST',
data,
})

View File

@ -36,7 +36,7 @@
:formData="
Object.assign({}, searchFormRef?.getFormData() || {}, {
pageNum: pagination.page,
pageSize: pagination.pageSize,
pageSize: pagination.limit,
}) || {}
"
/>
@ -133,7 +133,7 @@ const loading = ref(false)
const tableData = ref([])
const pagination = reactive({
page: 1,
pageSize: 10,
limit: 10,
total: 0,
})
@ -187,7 +187,7 @@ const fetchData = async (formData = {}) => {
...formData,
...props.defaultQueryParams,
pageNum: pagination.page, // pageNum
pageSize: pagination.pageSize, // pageSize
pageSize: pagination.limit, // pageSize
}
const response = await props.loadData(params)

View File

@ -112,7 +112,8 @@ function logout() {
})
.then(() => {
userStore.logOut().then(() => {
location.href = '/index'
// location.href = '/index'
location.href = import.meta.env.VITE_APP_PUBLIC_PATH + '/index'
})
})
.catch(() => {})
@ -130,136 +131,137 @@ function toggleTheme() {
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: var(--navbar-bg);
border-bottom: 1px solid var(--navbar-border, #f0f0f0);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
backdrop-filter: blur(12px);
height: 50px;
overflow: hidden;
position: relative;
background: var(--navbar-bg);
border-bottom: 1px solid var(--navbar-border, #f0f0f0);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
backdrop-filter: blur(12px);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.25s ease, transform 0.2s ease;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(22, 119, 255, 0.08);
transform: translateY(-1px);
}
}
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
display: flex;
align-items: center;
padding-right: 12px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 10px;
margin: 0 2px;
height: 100%;
font-size: 18px;
color: #4b5563;
vertical-align: middle;
border-radius: 999px;
transition: background 0.25s ease, color 0.25s ease, transform 0.2s ease, box-shadow 0.2s ease;
&.hover-effect {
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.25s ease, transform 0.2s ease;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(22, 119, 255, 0.08);
color: #1677ff;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
background: rgba(22, 119, 255, 0.08);
transform: translateY(-1px);
}
}
&.theme-switch-wrapper {
display: inline-flex;
align-items: center;
svg {
transition: transform 0.25s ease;
&:hover {
transform: scale(1.1);
}
}
}
}
.avatar-container {
margin-right: 0;
padding-right: 0;
.avatar-wrapper {
position: relative;
right: 0;
margin-top: 0;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(17, 24, 39, 0.02);
transition: background 0.25s ease, transform 0.2s ease, box-shadow 0.2s ease;
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18);
}
.user-nickname {
position: static;
font-size: 14px;
font-weight: 500;
color: #374151;
white-space: nowrap;
}
i {
display: none;
}
}
&:hover .avatar-wrapper {
background: rgba(22, 119, 255, 0.1);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.16);
}
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
display: flex;
align-items: center;
padding-right: 12px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 10px;
margin: 0 2px;
height: 100%;
font-size: 18px;
color: #4b5563;
vertical-align: middle;
border-radius: 999px;
transition: background 0.25s ease, color 0.25s ease, transform 0.2s ease,
box-shadow 0.2s ease;
&.hover-effect {
cursor: pointer;
&:hover {
background: rgba(22, 119, 255, 0.08);
color: #1677ff;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
}
}
&.theme-switch-wrapper {
display: inline-flex;
align-items: center;
svg {
transition: transform 0.25s ease;
&:hover {
transform: scale(1.1);
}
}
}
}
.avatar-container {
margin-right: 0;
padding-right: 0;
.avatar-wrapper {
position: relative;
right: 0;
margin-top: 0;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(17, 24, 39, 0.02);
transition: background 0.25s ease, transform 0.2s ease, box-shadow 0.2s ease;
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18);
}
.user-nickname {
position: static;
font-size: 14px;
font-weight: 500;
color: #374151;
white-space: nowrap;
}
i {
display: none;
}
}
&:hover .avatar-wrapper {
background: rgba(22, 119, 255, 0.1);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.16);
}
}
}
}
}
</style>

View File

@ -194,7 +194,7 @@ export const dynamicRoutes = [
]
const router = createRouter({
history: createWebHistory(),
history: createWebHistory(import.meta.env.VITE_APP_PUBLIC_PATH || '/'),
routes: constantRoutes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {

View File

@ -104,7 +104,8 @@ service.interceptors.response.use(
useUserStore()
.logOut()
.then(() => {
location.href = '/index'
// location.href = '/index'
location.href = import.meta.env.VITE_APP_PUBLIC_PATH + '/index'
})
})
.catch(() => {

View File

@ -15,12 +15,13 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],
dialogConfig: reactive({
outerVisible: false,
outerTitle: '新业务类型',
outerTitle: '新业务类型',
outerWidth: '720px',
minHeight: '320px',
maxHeight: '80vh',

View File

@ -122,7 +122,7 @@ const actionColumns = [
//
const onHandleAdd = () => {
editId.value = null
dialogConfig.outerTitle = '新业务类型'
dialogConfig.outerTitle = '新业务类型'
dialogConfig.outerVisible = true
//
addAndEditForm.value = getInitFormData()

View File

@ -15,12 +15,13 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],
dialogConfig: reactive({
outerVisible: false,
outerTitle: '新运检站',
outerTitle: '新运检站',
outerWidth: '720px',
minHeight: '320px',
maxHeight: '80vh',

View File

@ -132,7 +132,7 @@ const actionColumns = [
//
const onHandleAdd = () => {
editId.value = null
dialogConfig.outerTitle = '新运检站'
dialogConfig.outerTitle = '新运检站'
dialogConfig.outerVisible = true
//
addAndEditForm.value = getInitFormData()

View File

@ -15,6 +15,7 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],

View File

@ -15,6 +15,7 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],

View File

@ -4,23 +4,24 @@ export default {
{
type: 'input',
prop: 'planMajorName',
placeholder: '请输入业务类型名称',
placeholder: '请输入计划类别名称',
},
],
tableColumns: [
{
prop: 'planMajorName',
label: '业务类型名称',
label: '计划类别名称',
},
{
prop: 'remark',
label: '备注',
width: '200',
},
],
dialogConfig: reactive({
outerVisible: false,
outerTitle: '新增业务类型',
outerTitle: '新建计划类别',
outerWidth: '720px',
minHeight: '320px',
maxHeight: '80vh',

View File

@ -15,6 +15,7 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],

View File

@ -15,6 +15,7 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],

View File

@ -15,6 +15,7 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],

View File

@ -20,6 +20,7 @@ export default {
{
prop: 'remark',
label: '备注',
width: '200',
},
],

View File

@ -9,7 +9,7 @@ export const buildFormColumns = (
) => [
{
type: 'input',
prop: 'personName',
prop: 'name',
placeholder: '请输入姓名',
},
{

View File

@ -17,8 +17,8 @@
<el-switch
active-value="1"
inactive-value="0"
v-model="row.longTermSecondment"
@change="onHandleLongTermSecondmentChange(row)"
:model-value="getLongTermSecondmentValue(row)"
@change="onHandleLongTermSecondmentChange($event, row)"
/>
</template>
</ComTable>
@ -75,7 +75,7 @@ const editId = ref(null)
const { options: inspectionStationOptions } = useOptions(
'inspectionStationOptions',
getInspectionStationSelectAPI,
{ category: 0 },
{},
)
const { options: positionOptions } = useOptions(
'positionOptions',
@ -207,8 +207,32 @@ const onCloseDialogOuter = (visible) => {
}
}
// longTermSecondment
const getLongTermSecondmentValue = (row) => {
const value = row.longTermSecondment
if (value === null || value === undefined) {
return '0'
}
//
return String(value)
}
//
const onHandleLongTermSecondmentChange = (row) => {
console.log(row)
const onHandleLongTermSecondmentChange = (value, row) => {
// row switch
row.longTermSecondment = value
// '1' -> 1, '0' -> 0
const params = {
id: row.id,
longTermSecondment: value === '1' ? 1 : 0,
}
updatePersonAPI(params).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('更新成功')
comTableRef.value?.refresh()
}
})
}
</script>

View File

@ -1,102 +1,111 @@
<template>
<div class="monthly-add-form">
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="auto"
size="large"
class="month-form"
>
<el-row :gutter="12" align="middle">
<el-col :span="8">
<el-form-item label="计划执行月份" prop="month">
<el-date-picker
v-model="formData.month"
type="month"
value-format="YYYY-MM"
placeholder="请选择月份"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-form-item prop="keyword" class="keyword-item">
<el-input
placeholder="输入关键字"
clearable
v-model.trim="formData.keyword"
@keyup.enter="onSearch"
style="width: 240px"
<!-- 表单区域 -->
<div class="form-section">
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="140px"
size="large"
class="month-form"
>
<template #suffix>
<el-icon @click="onSearch" style="cursor: pointer">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="计划执行日期:" prop="nowDate" class="month-item">
<el-date-picker
type="date"
style="width: 200px"
v-model="formData.nowDate"
value-format="YYYY-MM-DD"
placeholder="请选择计划日期"
/>
</el-form-item>
</el-form>
<!-- 搜索区域 -->
<div class="search-section">
<el-input
clearable
@keyup.enter="onSearch"
class="search-input"
placeholder="输入关键字"
v-model.trim="formData.keyword"
>
<template #prefix>
<el-icon class="search-icon" @click="onSearch">
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
<!-- 任务列表 -->
<el-table
border
:data="tableData"
v-loading="loading"
@selection-change="onSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column align="center" prop="projectName" label="项目名称" min-width="260" />
<el-table-column align="center" prop="workTask" label="工作任务" min-width="360" />
</el-table>
<div class="table-section">
<el-table
:data="tableData"
v-loading="loading"
@selection-change="onSelectionChange"
class="task-table"
stripe
border
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 600 }"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column
prop="projectName"
label="项目名称"
min-width="260"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="workContent"
label="工作任务"
min-width="360"
show-overflow-tooltip
align="center"
/>
</el-table>
</div>
</div>
</template>
<script setup name="MonthlyPlanAddForm">
import { ref, watch, getCurrentInstance } from 'vue'
import { ref, watch, getCurrentInstance, reactive } from 'vue'
import { Search } from '@element-plus/icons-vue'
const props = defineProps({
//
defaultMonth: {
type: String,
default: '',
},
//
loadTaskList: {
type: Function,
default: null,
},
})
import { listDailyPlanPopupAPI } from '@/api/planMange/dailyPlan'
import dayjs from 'dayjs'
const { proxy } = getCurrentInstance()
const formRef = ref(null)
const loading = ref(false)
const tableData = ref([])
const tableData = ref([
{ name: '项目1', workTask: '工作任务1' },
{ name: '项目2', workTask: '工作任务2' },
])
const selectedRows = ref([])
const formData = ref({
month: props.defaultMonth || '',
nowDate: dayjs().add(1, 'day').format('YYYY-MM-DD'), // + 1
keyword: '',
})
const rules = {
month: [{ required: true, message: '请选择计划执行月份', trigger: 'change' }],
nowDate: [{ required: true, message: '请选择计划执行日期', trigger: 'change' }],
}
// /
const fetchData = async () => {
if (!props.loadTaskList) return
await formRef.value.validate()
loading.value = true
try {
const params = {
month: formData.value.month,
keyword: formData.value.keyword,
nowDate: formData.value.nowDate,
keyWord: formData.value.keyword,
}
const res = await props.loadTaskList(params)
tableData.value = res?.rows || res?.data?.rows || res?.data || []
const res = await listDailyPlanPopupAPI(params)
tableData.value = res?.data
} catch (e) {
console.error('加载计划任务列表失败:', e)
tableData.value = []
@ -113,31 +122,125 @@ const onSelectionChange = (rows) => {
selectedRows.value = rows
}
//
watch(
() => props.defaultMonth,
(val) => {
if (val) formData.value.month = val
},
{ immediate: true },
)
//
defineExpose({
getSelectedTasks: () => selectedRows.value,
getCurrentDate: () => formData.value.nowDate,
search: fetchData,
validate: () => formRef.value.validate(),
validate: async () => {
await formRef.value.validate()
if (selectedRows.value.length === 0) {
proxy.$modal.msgError('请选择项目')
return false
}
return true
},
})
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.monthly-add-form {
.month-form {
margin-bottom: 12px;
padding: 0;
.form-section {
margin-bottom: 20px;
.month-form {
margin-bottom: 16px;
.month-item {
margin-bottom: 0;
:deep(.el-form-item__label) {
font-weight: 500;
color: #303133;
&::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
}
:deep(.el-form-item__content) {
margin-left: 0 !important;
}
}
}
.search-section {
display: flex;
justify-content: flex-start;
margin-top: 16px;
.search-input {
width: 300px;
:deep(.el-input__wrapper) {
border-radius: 4px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
transition: all 0.3s;
&:hover {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
&.is-focus {
box-shadow: 0 0 0 1px #409eff inset;
}
}
.search-icon {
color: #909399;
cursor: pointer;
font-size: 16px;
transition: color 0.3s;
&:hover {
color: #409eff;
}
}
}
}
}
.keyword-item :deep(.el-form-item__content) {
justify-content: flex-end;
.table-section {
.task-table {
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
:deep(.el-table__header) {
th {
padding: 12px 0;
font-size: 14px;
}
}
:deep(.el-table__body) {
td {
padding: 12px 0;
font-size: 14px;
}
tr {
&:hover {
background-color: #f5f7fa;
}
}
}
:deep(.el-checkbox) {
.el-checkbox__input.is-checked .el-checkbox__inner {
background-color: #409eff;
border-color: #409eff;
}
}
}
}
}
</style>

View File

@ -55,11 +55,11 @@ export const buildFormColumns = () => [
// 月计划列表表格列
export const tableColumns = [
{ prop: 'stationName', label: '日期', fixed: true },
{ prop: 'majorName', label: '运检站', fixed: true },
{ prop: 'businessTypeName', label: '专业', fixed: true },
{ prop: 'dayPlan', label: '日期', fixed: true, width: '140' },
{ prop: 'inspectionStationName', label: '运检站', fixed: true },
{ prop: 'planMajorName', label: '专业', fixed: true },
{ prop: 'projectName', label: '项目名称', fixed: true },
{ prop: 'workTask', label: '工作任务' },
{ prop: 'workContent', label: '工作任务' },
{ prop: 'riskLevelName', label: '风险等级' },
{ prop: 'categoryName', label: '计划工作量(基)' },
]
@ -107,7 +107,7 @@ export const tableColumns_2 = [
export const dialogConfig = reactive({
outerVisible: false,
outerTitle: '新增计划',
outerTitle: '添加计划任务',
outerWidth: '70%', // 根据图片缩小宽度更美观
minHeight: '70vh',
maxHeight: '90vh',

View File

@ -1,301 +1,671 @@
<template>
<div class="app-container monthly-plan-edit">
<div class="app-container daily-plan-edit">
<!-- 顶部返回 + 标题 -->
<el-page-header @back="onBack" :content="pageTitle" class="page-header" />
<!-- 基本信息表单禁用 -->
<el-card shadow="never" class="card-section">
<template #header>
<div class="card-header">基本信息</div>
</template>
<el-form
ref="formRef"
ref="baseFormRef"
:model="formData"
:rules="rules"
label-width="auto"
:disabled="isDetail"
size="large"
label-width="180"
:disabled="true"
>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="运检站" prop="stationId">
<el-select
v-model="formData.stationId"
placeholder="请选择运检站"
clearable
filterable
>
<el-option
v-for="item in stationOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="运检站">
<el-input v-model="formData.inspectionStationName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="专业" prop="majorId">
<el-select
v-model="formData.majorId"
placeholder="请选择专业"
clearable
>
<el-option
v-for="item in majorOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="业务类型" prop="businessTypeId">
<el-select
v-model="formData.businessTypeId"
placeholder="请选择业务类型"
clearable
>
<el-option
v-for="item in businessTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-col :span="12">
<el-form-item label="专业">
<el-input v-model="formData.planMajorName" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model.trim="formData.projectName"
placeholder="请输入项目名称"
maxlength="50"
show-word-limit
clearable
/>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="项目名称">
<el-input v-model="formData.projectName" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="风险等级" prop="riskLevel">
<el-select
v-model="formData.riskLevel"
placeholder="请选择风险等级"
clearable
>
<el-option
v-for="item in riskLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="计划执行月份" prop="month">
<el-date-picker
v-model="formData.month"
type="month"
value-format="YYYY-MM"
placeholder="请选择月份"
style="width: 100%"
/>
<el-col :span="12">
<el-form-item label="日期">
<el-input v-model="formData.dayPlan" disabled />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="作业任务" prop="workContent">
<el-input
type="textarea"
v-model.trim="formData.workContent"
placeholder="请输入作业内容"
maxlength="500"
show-word-limit
:autosize="{ minRows: 3, maxRows: 6 }"
/>
</el-form-item>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="计划开始时间" prop="planStartDate">
<el-date-picker
v-model="formData.planStartDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择计划开始时间"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="计划结束时间" prop="planEndDate">
<el-date-picker
v-model="formData.planEndDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择计划结束时间"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
v-model.trim="formData.remark"
placeholder="请输入备注"
maxlength="200"
show-word-limit
:autosize="{ minRows: 3, maxRows: 6 }"
/>
</el-form-item>
</el-form>
</el-card>
<!-- 人员排班区域先留出结构后续可按业务补充 -->
<el-card shadow="never" class="card-section">
<template #header>
<div class="card-header">人员排班</div>
</template>
<div class="calendar-placeholder">
<span>这里展示月份日历和每日人员排班待与后端字段对接后完善</span>
</div>
</el-card>
<!-- 动态表单组件根据类型切换 -->
<component
:is="currentFormComponent"
:form-data="formData"
:is-detail="isDetail"
:selected-manager-names="selectedManagerNames"
@open-person-picker="onOpenPersonPicker"
@update:form-data="formData = $event"
ref="formRef"
/>
<!-- 底部操作按钮详情模式下隐藏 -->
<div v-if="!isDetail" class="page-footer">
<ComButton plain type="info" @click="onBack">取消</ComButton>
<ComButton type="primary" @click="onSubmit">保存</ComButton>
<!-- 底部操作按钮 -->
<div class="page-footer">
<ComButton plain type="info" @click="onBack">
{{ isDetail ? '返回' : '取消' }}
</ComButton>
<ComButton v-if="!isDetail" type="primary" @click="onSubmit">保存</ComButton>
</div>
<!-- 人员选择弹窗 -->
<ComDialog :dialog-config="managerDialogConfig" @closeDialogOuter="onCloseDialogOuter">
<template #outerContent>
<el-row :gutter="20">
<el-col :span="16">
<div class="person-search-bar">
<el-input
v-model.trim="managerDialog.keyword"
placeholder="输入姓名搜索"
clearable
prefix-icon="Search"
/>
</div>
<el-table
border
ref="personTableRef"
:data="filteredPersons"
@selection-change="onManagerSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column
show-overflow-tooltip
prop="org"
label="人员所属单位"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="name"
label="姓名"
width="120"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="gender"
label="性别"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="station"
label="岗位"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="position"
label="岗位性质"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="type"
label="人员分类"
align="center"
/>
</el-table>
</el-col>
<el-col :span="8">
<div class="selected-panel">
<div class="panel-header">
<span>已选人员</span>
<el-button type="text" @click="onClearManagers">清空</el-button>
</div>
<div class="selected-list">
<div>
<el-tag
:key="item.id"
:closable="!isDetail"
class="selected-tag"
@close="onRemoveManager(item)"
v-for="item in managerDialog.selected"
>
{{ item.name }}
</el-tag>
</div>
<div v-if="!managerDialog.selected.length" class="empty-tip">
暂未选择人员
</div>
</div>
</div>
</el-col>
</el-row>
<el-row class="common-btn-row" justify="end" style="margin-top: 16px">
<ComButton plain type="info" @click="managerDialogConfig.outerVisible = false">
取消
</ComButton>
<ComButton type="primary" @click="onConfirmManager">确定</ComButton>
</el-row>
</template>
</ComDialog>
</div>
</template>
<script setup name="MonthlyPlanEdit">
import { ref, computed, getCurrentInstance } from 'vue'
<script setup name="DailyPlanEdit">
import {
ref,
reactive,
computed,
getCurrentInstance,
nextTick,
onMounted,
shallowRef,
watch,
} from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { addPlanAPI, updatePlanAPI } from '@/api/planMange/plan.js'
import { getPersonnelCommonListAPI } from '@/api/common.js'
import { updateDailyPlanAPI } from '@/api/planMange/dailyPlan.js'
import { Search } from '@element-plus/icons-vue'
import { useOptions } from '@/hooks/useOptions'
import ComButton from '@/components/ComButton/index.vue'
import ComDialog from '@/components/ComDialog/index.vue'
import RunForm from './forms/runForm.vue'
import RepairForm from './forms/repairForm.vue'
import ProjectForm from './forms/projectForm.vue'
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const mode = computed(() => route.query.mode || 'add')
// 使 Hook
const { options: personnelCommonOptions } = useOptions(
'personnelCommonOptions',
getPersonnelCommonListAPI,
{},
)
const mode = computed(() => route.query.mode || 'edit')
const isDetail = computed(() => mode.value === 'detail')
const pageTitle = computed(() => {
if (mode.value === 'edit') return '编辑月计划'
if (mode.value === 'detail') return '月计划详情'
return '新增月计划'
// 0-1-2-
const dayPlanType = computed(() => {
//
if (route.query.dayPlanType !== undefined) {
return String(route.query.dayPlanType)
}
//
return '0'
})
//
const formComponents = {
0: RunForm, //
1: RepairForm, //
2: ProjectForm, //
}
const currentFormComponent = shallowRef(formComponents[dayPlanType.value] || RunForm)
const pageTitle = computed(() => {
const typeMap = {
0: '运行',
1: '检修',
2: '项目部',
}
const typeName = typeMap[dayPlanType.value] || '运行'
if (mode.value === 'edit') return `编辑日计划(${typeName}`
if (mode.value === 'detail') return `日计划详情(${typeName}`
return `新增日计划(${typeName}`
})
const baseFormRef = ref(null)
const formRef = ref(null)
const getInitFormData = () => ({
planId: null,
stationId: null,
majorId: null,
businessTypeId: null,
projectName: '',
workContent: '',
riskLevel: null,
month: '',
planStartDate: '',
planEndDate: '',
remark: '',
//
const routeParams = computed(() => ({
inspectionStationName: route.query.inspectionStationName || '',
projectName: route.query.projectName || '',
planMajorName: route.query.planMajorName || '',
dayPlan: route.query.dayPlan || '',
dayPlanId: route.query.id || null,
}))
//
const getRunFormData = () => ({
dayPlanId: routeParams.value.dayPlanId,
//
inspectionStationName: routeParams.value.inspectionStationName,
projectName: routeParams.value.projectName,
planMajorName: routeParams.value.planMajorName,
dayPlan: routeParams.value.dayPlan,
//
plannedWorkload: null, //
planPersonnelList: [], //
planPersonnel: '', // 使
proposedLongTimeCar: null, //
proposedTemporaryCar: null, //
//
actualPersonnelList: [], //
actualPersonnel: '', // 使
actualLongTimeCar: null, //
actualTemporaryCar: null, //
actualWorkContent: '', //
actualWorkload: null, //
completionPercentage: null, //
planCompletionStatus: null, //
planChanges: '', //
})
//
const getRepairFormData = () => ({
dayPlanId: routeParams.value.dayPlanId,
//
inspectionStationName: routeParams.value.inspectionStationName,
projectName: routeParams.value.projectName,
planMajorName: routeParams.value.planMajorName,
dayPlan: routeParams.value.dayPlan,
//
plannedWorkload: null, //
highAltitudePersonnelList: [], //
groundPersonnelList: [], //
skilledWorkerNum: null, //
skilledWorkerDay: null, //
auxiliaryWorkerNum: null, //
auxiliaryWorkerDay: null, //
proposedLongTimeCar: null, //
proposedTemporaryCar: null, //
proposedSubcontractedCar: null, //
//
actualHighAltitudePersonnelList: [], //
actualGroundPersonnelList: [], //
actualSkilledWorkerNum: null, //
actualAuxiliaryWorkerNum: null, //
actualLongTimeCar: null, //
actualTemporaryCar: null, //
actualSubcontractedCar: null, //
actualWorkloadList: [
{
workloadCategoryId: '',
workloadCategoryName: '',
unitPrice: '',
workloadNum: '',
},
], //
actualWorkload: null, //
completionPercentage: null, //
planCompletionStatus: null, //
planChanges: '', //
})
//
const getProjectFormData = () => ({
dayPlanId: routeParams.value.dayPlanId,
//
inspectionStationName: routeParams.value.inspectionStationName,
projectName: routeParams.value.projectName,
planMajorName: routeParams.value.planMajorName,
dayPlan: routeParams.value.dayPlan,
//
// TODO:
planPersonnelList: [], //
planPersonnel: '', // 使
//
actualPersonnelList: [], //
actualPersonnel: '', // 使
// TODO:
})
//
const getInitFormData = () => {
const type = dayPlanType.value
if (type === '0' || type === 0) {
return getRunFormData()
} else if (type === '1' || type === 1) {
return getRepairFormData()
} else if (type === '2' || type === 2) {
return getProjectFormData()
}
//
return getRunFormData()
}
const formData = ref(getInitFormData())
//
const stationOptions = [
{ label: '昆明运检站', value: 1 },
{ label: '大理运检站', value: 2 },
]
const majorOptions = [
{ label: '输电专业', value: 1 },
{ label: '变电专业', value: 2 },
]
const businessTypeOptions = [
{ label: '日常巡视', value: 1 },
{ label: '缺陷处理', value: 2 },
]
const riskLevelOptions = [
{ label: '低风险', value: 1 },
{ label: '中风险', value: 2 },
{ label: '高风险', value: 3 },
]
const rules = {
stationId: [{ required: true, message: '请选择运检站', trigger: 'change' }],
majorId: [{ required: true, message: '请选择专业', trigger: 'change' }],
businessTypeId: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
workContent: [{ required: true, message: '请输入作业内容', trigger: 'blur' }],
riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
month: [{ required: true, message: '请选择计划执行月份', trigger: 'change' }],
planStartDate: [{ required: true, message: '请选择计划开始时间', trigger: 'change' }],
planEndDate: [{ required: true, message: '请选择计划结束时间', trigger: 'change' }],
}
//
const rules = computed(() => ({}))
const onBack = () => {
router.back()
}
const onSubmit = () => {
formRef.value.validate(async (valid) => {
if (!valid) return
const API = mode.value === 'edit' ? updatePlanAPI : addPlanAPI
const payload = { ...formData.value }
try {
const res = await API(payload)
if (res.code === 200) {
proxy.$modal.msgSuccess(mode.value === 'edit' ? '编辑成功' : '新增成功')
onBack()
}
} catch (error) {
console.error('保存月计划失败:', error)
}
proxy.$tab.closeOpenPage({
path: '/plan/dailyPlan',
})
}
const onSubmit = async () => {
try {
// validate Promise<boolean>
const valid = await formRef.value?.validate()
console.log(valid, 'valid')
if (!valid) {
console.log('校验没有通过')
//
// validate fields
return
}
//
const proposedPersonnelList = []
formData.value.planPersonnelList.forEach((item) => {
proposedPersonnelList.push({
dayPlanId: route.query.id,
inspectionStationName: item.inspectionStationName,
personnelld: item.id,
name: item.name,
dataSource: 0,
})
})
formData.value.actualPersonnelList.forEach((item) => {
proposedPersonnelList.push({
dayPlanId: route.query.id,
inspectionStationName: item.inspectionStationName,
personnelld: item.id,
name: item.name,
dataSource: 1,
})
})
//
const {
inspectionStationName,
projectName,
planMajorName,
dayPlan,
planPersonnelList,
actualPersonnelList,
actualPersonnel,
planPersonnel,
proposedLongTimeCar,
...submitData
} = formData.value
submitData.proposedPersonnelList = proposedPersonnelList
const result = await updateDailyPlanAPI(submitData)
if (result.code === 200) {
proxy.$modal.msgSuccess('保存成功')
onBack()
}
} catch (error) {
console.log(error, 'error')
return Promise.reject(error)
}
}
//
const personTableRef = ref(null)
const managerDialogConfig = reactive({
outerVisible: false,
outerTitle: '选择人员',
outerWidth: '70%',
minHeight: '60vh',
maxHeight: '80vh',
})
const managerDialog = reactive({
visible: false,
keyword: '',
selected: [],
type: 'plan', // 'plan', 'actual', 'highAltitude', 'ground', 'actual_highAltitude', 'actual_ground'
})
//
const personList = computed(() => {
return (personnelCommonOptions.value || []).map((item) => ({
...item,
org: item.inspectionStationName, //
gender: item.sex === '1' ? '男' : '女',
station: item.positionName, //
position: item.personnelNatureName, //
type: item.personnelClassificationName, //
}))
})
const filteredPersons = computed(() => {
const list = personList.value
if (!managerDialog.keyword) return list
const keyword = managerDialog.keyword.toLowerCase()
return list.filter((item) => {
const name = (item.name || '').toLowerCase()
const org = (item.org || '').toLowerCase()
const station = (item.station || '').toLowerCase()
return name.includes(keyword) || org.includes(keyword) || station.includes(keyword)
})
})
const selectedManagerNames = computed(() => {
//
if (dayPlanType.value === '1' || dayPlanType.value === 1) {
//
const highAltitude = (formData.value.highAltitudePersonnelList || [])
.map((item) => item.name)
.join('、')
const ground = (formData.value.groundPersonnelList || [])
.map((item) => item.name)
.join('、')
return [highAltitude, ground].filter(Boolean).join('、') || ''
}
// 使 planPersonnelList
return (formData.value.planPersonnelList || []).map((item) => item.name).join('、')
})
const onOpenPersonPicker = async (type = 'plan') => {
managerDialog.type = type
managerDialogConfig.outerVisible = true
await nextTick()
if (personTableRef.value) {
personTableRef.value.clearSelection()
//
let currentList = []
if (type === 'plan') {
currentList = formData.value.planPersonnelList || []
} else if (type === 'actual') {
currentList = formData.value.actualPersonnelList || []
} else if (type === 'highAltitude') {
currentList = formData.value.highAltitudePersonnelList || []
} else if (type === 'ground') {
currentList = formData.value.groundPersonnelList || []
} else if (type === 'actual_highAltitude') {
currentList = formData.value.actualHighAltitudePersonnelList || []
} else if (type === 'actual_ground') {
currentList = formData.value.actualGroundPersonnelList || []
}
personList.value.forEach((row) => {
const exists = currentList.find((item) => item.id === row.id)
if (exists) {
personTableRef.value.toggleRowSelection(row, true)
}
})
managerDialog.selected = [...currentList]
}
}
const onManagerSelectionChange = (rows) => {
managerDialog.selected = [...rows]
}
const onRemoveManager = (item) => {
managerDialog.selected = managerDialog.selected.filter((row) => row.id !== item.id)
if (personTableRef.value) {
const target = personList.value.find((row) => row.id === item.id)
if (target) personTableRef.value.toggleRowSelection(target, false)
}
}
const onClearManagers = () => {
managerDialog.selected = []
if (personTableRef.value) personTableRef.value.clearSelection()
}
const onConfirmManager = () => {
const type = managerDialog.type
//
if (type === 'plan') {
formData.value.planPersonnelList = [...managerDialog.selected]
nextTick(() => {
formRef.value?.validateField?.('planPersonnelList')
})
} else if (type === 'actual') {
formData.value.actualPersonnelList = [...managerDialog.selected]
nextTick(() => {
formRef.value?.validateField?.('actualPersonnelList')
})
} else if (type === 'highAltitude') {
formData.value.highAltitudePersonnelList = [...managerDialog.selected]
nextTick(() => {
formRef.value?.validateField?.('highAltitudePersonnelList')
})
} else if (type === 'ground') {
formData.value.groundPersonnelList = [...managerDialog.selected]
nextTick(() => {
formRef.value?.validateField?.('groundPersonnelList')
})
} else if (type === 'actual_highAltitude') {
formData.value.actualHighAltitudePersonnelList = [...managerDialog.selected]
nextTick(() => {
formRef.value?.validateField?.('actualHighAltitudePersonnelList')
})
} else if (type === 'actual_ground') {
formData.value.actualGroundPersonnelList = [...managerDialog.selected]
nextTick(() => {
formRef.value?.validateField?.('actualGroundPersonnelList')
})
}
managerDialogConfig.outerVisible = false
}
//
const onCloseDialogOuter = (visible) => {
managerDialogConfig.outerVisible = visible
}
//
const getDetail = async () => {
// TODO: API
// const result = await getDailyPlanDetailAPI({ dayPlanId: route.query.id })
// if (result.code === 200 && result.data) {
// const data = result.data
// //
// }
}
//
watch(
() => dayPlanType.value,
(newType) => {
currentFormComponent.value = formComponents[newType] || RunForm
//
formData.value = getInitFormData()
},
{ immediate: true },
)
onMounted(() => {
if (route.query.id) {
getDetail()
}
})
</script>
<style scoped lang="scss">
.monthly-plan-edit {
.daily-plan-edit {
.page-header {
margin-bottom: 16px;
margin-bottom: 20px;
padding: 16px 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
:deep(.el-page-header__left) {
.el-page-header__back {
padding: 8px;
border-radius: 6px;
transition: all 0.3s ease;
background-color: transparent;
&:hover {
background-color: #f0f2f5;
color: #1677ff;
transform: translateX(-2px);
}
&::before {
font-size: 16px;
font-weight: 600;
}
}
}
:deep(.el-page-header__content) {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin-left: 12px;
}
}
.card-section {
margin-bottom: 16px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
:deep(.el-card__header) {
padding: 16px 20px;
background-color: #fafbfc;
border-bottom: 1px solid #f0f2f5;
}
:deep(.el-card__body) {
padding: 24px 20px;
}
}
.card-header {
font-weight: 600;
font-size: 14px;
}
.calendar-placeholder {
min-height: 240px;
font-size: 15px;
color: #1f2937;
position: relative;
padding-left: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
border: 1px dashed #e5e7eb;
border-radius: 8px;
background-color: #f9fafb;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
border-radius: 2px;
}
}
.page-footer {
@ -304,5 +674,46 @@ const onSubmit = () => {
justify-content: flex-end;
gap: 12px;
}
.clickable-suffix {
cursor: pointer;
}
}
.person-search-bar {
margin-bottom: 8px;
}
.selected-panel {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.selected-list {
flex: 1;
overflow: auto;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.selected-tag {
margin-left: 6px;
margin-bottom: 10px;
}
.empty-tip {
color: #9ca3af;
}
</style>

View File

@ -0,0 +1,175 @@
<template>
<el-card shadow="never" class="card-section">
<template #header>
<div class="card-header">投入的资源情况项目部</div>
</template>
<el-form
ref="formRef"
:model="formData"
:rules="!isDetail ? rules : {}"
size="large"
label-width="auto"
:disabled="isDetail"
>
<!-- 项目部类型的表单字段稍后完善 -->
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="计划工作量" prop="planWorkload">
<el-input-number
:model-value="formData.planWorkload"
@update:model-value="(val) => updateField('planWorkload', val)"
:min="0"
:precision="2"
placeholder="请输入计划工作量"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="拟投入作业人员" prop="planPersonnelList">
<el-input
:model-value="selectedManagerNames"
placeholder="请选择作业人员"
readonly
@click="onOpenPersonPicker"
class="clickable-suffix"
>
<template #suffix>
<el-icon class="clickable-suffix">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="拟投入长租车" prop="planLongRentCar">
<el-input-number
:model-value="formData.planLongRentCar"
@update:model-value="(val) => updateField('planLongRentCar', val)"
:min="0"
placeholder="请输入长租车数量"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="拟投入临租车" prop="planTempRentCar">
<el-input-number
:model-value="formData.planTempRentCar"
@update:model-value="(val) => updateField('planTempRentCar', val)"
:min="0"
placeholder="请输入临租车数量"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</template>
<script setup name="ProjectForm">
import { ref, computed } from 'vue'
import { Search } from '@element-plus/icons-vue'
const props = defineProps({
formData: {
type: Object,
required: true,
},
isDetail: {
type: Boolean,
default: false,
},
selectedManagerNames: {
type: String,
default: '',
},
})
const emit = defineEmits(['open-person-picker', 'update:formData'])
const formRef = ref(null)
//
const updateField = (field, value) => {
emit('update:formData', {
...props.formData,
[field]: value,
})
}
//
const rules = computed(() => ({
planWorkload: [{ required: true, message: '请输入计划工作量', trigger: 'blur' }],
planPersonnelList: [{ required: true, message: '请选择拟投入作业人员', trigger: 'change' }],
planLongRentCar: [{ required: true, message: '请输入拟投入长租车数量', trigger: 'blur' }],
planTempRentCar: [{ required: true, message: '请输入拟投入临租车数量', trigger: 'blur' }],
}))
const onOpenPersonPicker = () => {
emit('open-person-picker')
}
//
defineExpose({
validate: () => formRef.value?.validate(),
clearValidate: () => formRef.value?.clearValidate(),
})
</script>
<style scoped lang="scss">
.card-section {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
:deep(.el-card__header) {
padding: 16px 20px;
background-color: #fafbfc;
border-bottom: 1px solid #f0f2f5;
}
:deep(.el-card__body) {
padding: 24px 20px;
}
}
.card-header {
font-weight: 600;
font-size: 15px;
color: #1f2937;
position: relative;
padding-left: 12px;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
border-radius: 2px;
}
}
.clickable-suffix {
cursor: pointer;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,591 @@
<template>
<div>
<!-- 计划投入资源情况 -->
<el-card shadow="never" class="card-section">
<template #header>
<div class="card-header">计划投入资源情况</div>
</template>
<el-form
ref="formRef"
:model="formData"
:rules="!isDetail ? rules : {}"
size="large"
label-width="140"
:disabled="isDetail"
>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="计划工作量" prop="plannedWorkload">
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="请输入计划工作量"
:model-value="formData.plannedWorkload"
@update:model-value="(val) => updateField('plannedWorkload', val)"
>
<template #suffix>
<span class="input-suffix"></span>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="拟投入作业人员" prop="planPersonnelList">
<el-input
readonly
class="clickable-suffix"
@click="onOpenPersonPicker"
placeholder="请选择作业人员"
:model-value="selectedManagerNames"
>
<template #suffix>
<el-icon class="clickable-suffix">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="拟投入长租车" prop="proposedLongTimeCar">
<el-input
:model-value="formData.proposedLongTimeCar"
@update:model-value="
(val) => updateField('proposedLongTimeCar', val)
"
placeholder="请输入长租车数量"
clearable
maxlength="7"
show-word-limit
>
<template #suffix>
<span class="input-suffix"></span>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="拟投入临租车" prop="proposedTemporaryCar">
<el-input
:model-value="formData.proposedTemporaryCar"
@update:model-value="
(val) => updateField('proposedTemporaryCar', val)
"
placeholder="请输入临租车数量"
clearable
maxlength="7"
show-word-limit
>
<template #suffix>
<span class="input-suffix"></span>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 实际完成情况默认隐藏 -->
<el-card
v-if="showActualCompletion"
shadow="never"
class="card-section"
style="margin-top: 20px"
>
<template #header>
<div class="card-header">实际完成情况</div>
</template>
<el-form
ref="actualFormRef"
:model="formData"
:rules="!isDetail ? actualRules : {}"
size="large"
label-width="140"
:disabled="isDetail"
>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="实际入作业人员" prop="actualPersonnelList">
<el-input
:model-value="selectedActualManagerNames"
placeholder="请选择作业人员"
readonly
@click="onOpenActualPersonPicker"
class="clickable-suffix"
>
<template #suffix>
<el-icon class="clickable-suffix">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际入长租车" prop="actualLongTimeCar">
<el-input
:model-value="formData.actualLongTimeCar"
@update:model-value="(val) => updateField('actualLongTimeCar', val)"
placeholder="请输入长租车数量"
clearable
maxlength="7"
show-word-limit
>
<template #suffix>
<span class="input-suffix"></span>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="实际入临租车" prop="actualTemporaryCar">
<el-input
:model-value="formData.actualTemporaryCar"
@update:model-value="
(val) => updateField('actualTemporaryCar', val)
"
placeholder="请输入临租车数量"
clearable
maxlength="7"
show-word-limit
>
<template #suffix>
<span class="input-suffix"></span>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="完成比例" prop="completionPercentage">
<el-input
:model-value="calculatedCompletionPercentage"
placeholder="自动计算"
readonly
disabled
>
<template #suffix>
<span class="input-suffix">%</span>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="实际完成工作内容" prop="actualWorkContent">
<el-input
:model-value="formData.actualWorkContent"
@update:model-value="(val) => updateField('actualWorkContent', val)"
type="textarea"
:autosize="{ minRows: 6, maxRows: 12 }"
placeholder="请输入实际完成工作内容"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="实际完成工作量" prop="actualWorkload">
<el-input
:model-value="formData.actualWorkload"
@update:model-value="(val) => updateField('actualWorkload', val)"
placeholder="请输入实际完成工作量"
clearable
maxlength="7"
show-word-limit
>
<template #suffix>
<span class="input-suffix"></span>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作业计划完成情况" prop="planCompletionStatus">
<el-select
:model-value="formData.planCompletionStatus"
@update:model-value="
(val) => updateField('planCompletionStatus', val)
"
placeholder="请选择作业计划完成情况"
clearable
style="width: 100%"
>
<el-option
v-for="item in planCompletionStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="计划变更及未完成情况说明" prop="planChanges">
<el-input
:model-value="formData.planChanges"
@update:model-value="(val) => updateField('planChanges', val)"
type="textarea"
:autosize="{ minRows: 6, maxRows: 12 }"
placeholder="请输入计划变更及未完成情况说明"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮 -->
<div class="form-actions" v-if="!isDetail">
<ComButton
v-if="!showActualCompletion"
type="primary"
@click="showActualCompletion = true"
>
填写实际完成情况
</ComButton>
<ComButton v-else plain type="info" @click="onCancelActualCompletion">
取消实际完成情况
</ComButton>
</div>
</div>
</template>
<script setup name="RunForm">
import { ref, computed, nextTick, watch } from 'vue'
import { Search } from '@element-plus/icons-vue'
import ComButton from '@/components/ComButton/index.vue'
const props = defineProps({
formData: {
type: Object,
required: true,
},
isDetail: {
type: Boolean,
default: false,
},
selectedManagerNames: {
type: String,
default: '',
},
})
const emit = defineEmits(['open-person-picker', 'update:formData', 'open-actual-person-picker'])
const formRef = ref(null)
const actualFormRef = ref(null)
const showActualCompletion = ref(false)
//
const planCompletionStatusOptions = [
{ label: '已完成', value: '1' },
{ label: '部分完成', value: '2' },
{ label: '未完成', value: '3' },
{ label: '已取消', value: '4' },
]
//
const updateField = (field, value) => {
const newData = {
...props.formData,
[field]: value,
}
//
if (field === 'actualWorkload' || field === 'plannedWorkload') {
newData.completionPercentage = calculateCompletionPercentage(
newData.actualWorkload,
newData.plannedWorkload,
)
}
emit('update:formData', newData)
}
// ÷ × 100%
const calculateCompletionPercentage = (actualWorkload, plannedWorkload) => {
try {
//
const actual = Number(actualWorkload)
const planned = Number(plannedWorkload)
//
if (isNaN(actual) || isNaN(planned)) {
return null
}
// 0
if (planned <= 0) {
return null
}
//
if (actual < 0) {
return null
}
// 2
const percentage = (actual / planned) * 100
const roundedPercentage = Math.round(percentage * 100) / 100
// 100%100%
return roundedPercentage
} catch (error) {
console.error('计算完成比例时出错:', error)
return null
}
}
//
const calculatedCompletionPercentage = computed(() => {
const percentage = calculateCompletionPercentage(
props.formData.actualWorkload,
props.formData.plannedWorkload,
)
return percentage !== null ? percentage : ''
})
//
watch(
() => [props.formData.actualWorkload, props.formData.plannedWorkload],
([actualWorkload, plannedWorkload]) => {
if (showActualCompletion.value) {
const percentage = calculateCompletionPercentage(actualWorkload, plannedWorkload)
if (percentage !== null && percentage !== props.formData.completionPercentage) {
// 使 emit
emit('update:formData', {
...props.formData,
completionPercentage: percentage,
})
}
}
},
{ immediate: true },
)
//
watch(
() => showActualCompletion.value,
(isShow) => {
if (isShow && props.formData.actualWorkload && props.formData.plannedWorkload) {
const percentage = calculateCompletionPercentage(
props.formData.actualWorkload,
props.formData.plannedWorkload,
)
if (percentage !== null) {
emit('update:formData', {
...props.formData,
completionPercentage: percentage,
})
}
}
},
)
//
const positiveIntegerPattern = /^[1-9]\d*$/
// 0
const nonNegativeIntegerPattern = /^(0|[1-9]\d*)$/
// 0-100
const percentagePattern = /^(0|[1-9]\d?|100)$/
//
const rules = computed(() => ({
plannedWorkload: [
{ required: true, message: '请输入计划工作量', trigger: 'blur' },
{ pattern: positiveIntegerPattern, message: '请输入正整数', trigger: 'blur' },
],
planPersonnelList: [{ required: true, message: '请选择拟投入作业人员', trigger: 'change' }],
proposedLongTimeCar: [
{ required: true, message: '请输入拟投入长租车数量', trigger: 'blur' },
{ pattern: nonNegativeIntegerPattern, message: '请输入非负整数', trigger: 'blur' },
],
proposedTemporaryCar: [
{ required: true, message: '请输入拟投入临租车数量', trigger: 'blur' },
{ pattern: nonNegativeIntegerPattern, message: '请输入非负整数', trigger: 'blur' },
],
}))
//
const actualRules = computed(() => ({
actualPersonnelList: [{ required: true, message: '请选择实际入作业人员', trigger: 'change' }],
actualLongTimeCar: [
{ required: true, message: '请输入实际入长租车数量', trigger: 'blur' },
{ pattern: positiveIntegerPattern, message: '请输入非负整数', trigger: 'blur' },
],
actualTemporaryCar: [
{ required: true, message: '请输入实际入临租车数量', trigger: 'blur' },
{ pattern: positiveIntegerPattern, message: '请输入非负整数', trigger: 'blur' },
],
actualWorkContent: [{ required: true, message: '请输入实际完成工作内容', trigger: 'blur' }],
actualWorkload: [
{ required: true, message: '请输入实际完成工作量', trigger: 'blur' },
{ pattern: positiveIntegerPattern, message: '请输入正整数', trigger: 'blur' },
],
completionPercentage: [
{
required: true,
validator: (rule, value, callback) => {
const percentage = calculateCompletionPercentage(
props.formData.actualWorkload,
props.formData.plannedWorkload,
)
if (percentage === null || percentage === '') {
callback(new Error('请先填写计划工作量和实际完成工作量'))
} else {
callback()
}
},
trigger: 'blur',
},
],
planCompletionStatus: [
{ required: true, message: '请选择作业计划完成情况', trigger: 'change' },
],
planChanges: [{ required: false, message: '请输入计划变更及未完成情况说明', trigger: 'blur' }],
}))
//
const selectedActualManagerNames = computed(() =>
(props.formData.actualPersonnelList || []).map((item) => item.name).join('、'),
)
const onOpenPersonPicker = () => {
emit('open-person-picker', 'plan')
}
const onOpenActualPersonPicker = () => {
emit('open-person-picker', 'actual')
}
//
const onCancelActualCompletion = () => {
showActualCompletion.value = false
//
const clearedData = {
...props.formData,
actualPersonnelList: [],
proposedLongTimeCar: null,
proposedTemporaryCar: null,
actualWorkContent: '',
actualWorkload: null,
completionPercentage: null,
planCompletionStatus: null,
planChanges: '',
}
emit('update:formData', clearedData)
//
nextTick(() => {
actualFormRef.value?.clearValidate()
})
}
//
defineExpose({
validate: async () => {
const results = []
if (formRef.value) {
results.push(await formRef.value.validate())
}
if (showActualCompletion.value && actualFormRef.value) {
results.push(await actualFormRef.value.validate())
}
return results.every((r) => r !== false)
},
clearValidate: () => {
formRef.value?.clearValidate()
actualFormRef.value?.clearValidate()
},
scrollToField: (prop) => {
if (formRef.value) {
formRef.value.scrollToField(prop)
}
if (actualFormRef.value) {
actualFormRef.value.scrollToField(prop)
}
},
})
</script>
<style scoped lang="scss">
.card-section {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
:deep(.el-card__header) {
padding: 16px 20px;
background-color: #fafbfc;
border-bottom: 1px solid #f0f2f5;
}
:deep(.el-card__body) {
padding: 24px 20px;
}
}
.card-header {
font-weight: 600;
font-size: 15px;
color: #1f2937;
position: relative;
padding-left: 12px;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
border-radius: 2px;
}
}
.clickable-suffix {
cursor: pointer;
}
.input-suffix {
color: #909399;
font-size: 14px;
padding-right: 8px;
}
.form-actions {
margin-top: 20px;
display: flex;
justify-content: flex-start;
}
</style>

View File

@ -2,9 +2,9 @@
<div class="app-container">
<el-card class="top-radio">
<el-radio-group v-model="tableType">
<el-radio-button label="运行" value="运行" />
<el-radio-button label="检修" value="检修" />
<el-radio-button label="项目部" value="项目部" />
<el-radio-button label="运行" value="0" />
<el-radio-button label="检修" value="1" />
<el-radio-button label="项目部" value="2" />
</el-radio-group>
<ComButton type="info" plain icon="Download"> 下载统计表 </ComButton>
@ -15,13 +15,15 @@
:key="tableType"
:show-toolbar="true"
:show-action="true"
:load-data="listPlanAPI"
:form-columns="formColumns"
:load-data="listDailyPlanAPI"
:table-columns="initTableColumns"
:action-columns="actionColumns"
:defaultQueryParams="{ dayPlanType: tableType }"
>
<template #toolbar>
<ComButton type="primary" icon="Plus" @click="onHandleAdd">新增日计划</ComButton>
<ComButton type="primary" icon="Plus" @click="onHandleEdit">编辑调试</ComButton>
<ComButton
plain
type="info"
@ -49,6 +51,7 @@
import { ref, computed, getCurrentInstance } from 'vue'
import { useRouter } from 'vue-router'
import { listPlanAPI, delPlanAPI } from '@/api/planMange/plan.js'
import { listDailyPlanAPI, delDailyPlanAPI, addDailyPlanAPI } from '@/api/planMange/dailyPlan.js'
import config from './config'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
@ -62,17 +65,17 @@ const { tableColumns, tableColumns_1, tableColumns_2, dialogConfig, buildFormCol
const comTableRef = ref(null)
const addFormRef = ref(null)
const tableType = ref('运行')
const tableType = ref('0')
// 使
const formColumns = computed(() => buildFormColumns())
const initTableColumns = computed(() => {
if (tableType.value === '运行') return [...tableColumns, ...tableColumns_1]
if (tableType.value !== '运行') return [...tableColumns, ...tableColumns_2]
if (tableType.value === '0') return [...tableColumns, ...tableColumns_1]
if (tableType.value !== '0') return [...tableColumns, ...tableColumns_2]
})
//
const actionColumns = [
const actionColumns = computed(() => [
{
label: '详情',
type: 'primary',
@ -80,7 +83,15 @@ const actionColumns = [
handler: (row) => {
router.push({
path: '/plan/dailyPlanEdit/index',
query: { id: row.planId, mode: 'detail' },
query: {
id: row.planId || row.dayPlanId,
mode: 'detail',
dayPlanType: tableType.value,
inspectionStationName: row.inspectionStationName,
projectName: row.projectName,
planMajorName: row.planMajorName,
dayPlan: row.dayPlan,
},
})
},
},
@ -91,7 +102,15 @@ const actionColumns = [
handler: (row) => {
router.push({
path: '/plan/dailyPlanEdit/index',
query: { id: row.planId, mode: 'edit' },
query: {
id: row.planId || row.dayPlanId,
mode: 'edit',
dayPlanType: tableType.value,
inspectionStationName: row.inspectionStationName,
projectName: row.projectName,
planMajorName: row.planMajorName,
dayPlan: row.dayPlan,
},
})
},
},
@ -101,7 +120,7 @@ const actionColumns = [
link: true,
handler: (row) => {
proxy.$modal.confirm('是否确认删除该月计划?').then(async () => {
const result = await delPlanAPI({ planId: row.planId })
const result = await delDailyPlanAPI({ dayPlanId: row.dayPlanId })
if (result.code === 200) {
proxy.$modal.msgSuccess('删除成功')
comTableRef.value?.refresh()
@ -109,7 +128,7 @@ const actionColumns = [
})
},
},
]
])
//
const onHandleAdd = () => {
@ -122,14 +141,38 @@ const onHandleCancel = () => {
}
//
const onHandleSave = () => {
addFormRef.value.validate()
const onHandleSave = async () => {
await addFormRef.value.validate()
const paramsList = addFormRef.value.getSelectedTasks().map((item) => ({
dayPlan: addFormRef.value.getCurrentDate(),
monthlyPlanId: item.monthlyPlanId,
dayPlanType: tableType.value,
riskLevel: item.riskLevel,
}))
const result = await addDailyPlanAPI(paramsList)
if (result.code === 200) {
proxy.$modal.msgSuccess('保存成功')
dialogConfig.outerVisible = false
comTableRef.value?.refresh()
}
}
//
const onCloseDialogOuter = (visible) => {
dialogConfig.outerVisible = visible
}
//
const onHandleEdit = () => {
router.push({
path: '/plan/dailyPlanEdit/index',
query: {
mode: 'edit',
dayPlanType: tableType.value,
},
})
}
</script>
<style scoped lang="scss">

View File

@ -7,7 +7,7 @@
:model="formData"
:rules="rules"
label-width="140px"
size="default"
size="large"
class="month-form"
>
<el-form-item label="月计划执行月份:" prop="month" class="month-item">
@ -47,6 +47,7 @@
@selection-change="onSelectionChange"
class="task-table"
stripe
border
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 600 }"
>
<el-table-column type="selection" width="55" align="center" />
@ -54,8 +55,8 @@
prop="projectName"
label="项目名称"
min-width="260"
show-overflow-tooltip
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="workContent"
@ -75,21 +76,7 @@ import { Search } from '@element-plus/icons-vue'
import { listMonthlyPlanPopupAPI } from '@/api/planMange/monthlyPlan'
import dayjs from 'dayjs'
const props = defineProps({
//
defaultMonth: {
type: String,
default: '',
},
//
loadTaskList: {
type: Function,
default: null,
},
})
const { proxy } = getCurrentInstance()
const formRef = ref(null)
const loading = ref(false)
const tableData = ref([
@ -134,15 +121,6 @@ const onSelectionChange = (rows) => {
selectedRows.value = rows
}
//
watch(
() => props.defaultMonth,
(val) => {
if (val) formData.value.month = val
},
{ immediate: true },
)
//
defineExpose({
getSelectedTasks: () => selectedRows.value,

View File

@ -55,13 +55,13 @@ export const buildFormColumns = (
// 月计划列表表格列
export const tableColumns = [
{ prop: 'inspectionStationName', label: '运检站', fixed: true },
{ prop: 'planMajorId', label: '专业', fixed: true },
{ prop: 'businessTypeId', label: '业务类型', fixed: true },
{ prop: 'projectName', label: '项目名称', fixed: true },
{ prop: 'inspectionStationName', label: '运检站', fixed: true, width: '180' },
{ prop: 'planMajorName', label: '专业', fixed: true },
{ prop: 'businessTypeName', label: '业务类型', fixed: true },
{ prop: 'projectName', label: '项目名称', fixed: true, width: '180' },
{ prop: 'workContent', label: '工作任务' },
{ prop: 'riskLevel', label: '风险等级' },
{ prop: 'planCategoryId', label: '类别' },
{ prop: 'planCategoryName', label: '类别' },
{ prop: 'workAmount', label: '工作量', width: '180', slot: 'workAmount' },
{ prop: 'towerBaseNumber', label: '基塔数' },
{ prop: 'plannedStartTime', label: '计划开始时间', width: '140' },
@ -70,7 +70,7 @@ export const tableColumns = [
prop: 'planPersonnel',
label: '计划投入管理人员数量',
formatter: (row) => {
return row.personneltList?.length || 0
return row.personneltList?.length || ''
},
},
@ -83,7 +83,7 @@ export const tableColumns = [
formatter: (row) => {
return (
(dayjs(row.plannedEndTime).diff(dayjs(row.plannedStartTime), 'day') + 1) *
row.planSkilledWorkerDay
row.planSkilledWorkerDay || ''
)
},
},
@ -95,7 +95,7 @@ export const tableColumns = [
formatter: (row) => {
return (
(dayjs(row.plannedEndTime).diff(dayjs(row.plannedStartTime), 'day') + 1) *
row.planAuxiliaryWorkerDay
row.planAuxiliaryWorkerDay || ''
)
},
},
@ -105,7 +105,7 @@ export const tableColumns = [
export const dialogConfig = reactive({
outerVisible: false,
outerTitle: '新增计划',
outerTitle: '添加计划任务',
outerWidth: '70%', // 根据图片缩小宽度更美观
minHeight: '70vh',
maxHeight: '90vh',

View File

@ -11,9 +11,9 @@
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="160"
:rules="!isDetail ? rules : {}"
size="large"
label-width="auto"
:disabled="isDetail"
>
<el-row :gutter="24">
@ -74,12 +74,12 @@
<el-col :span="24">
<el-form-item label="工作任务">
<el-input
type="textarea"
v-model.trim="formData.workContent"
placeholder="请输入作业内容"
disabled
maxlength="500"
show-word-limit
disabled
type="textarea"
placeholder="请输入作业内容"
v-model.trim="formData.workContent"
:autosize="{ minRows: 4, maxRows: 12 }"
/>
</el-form-item>
@ -90,15 +90,15 @@
<el-col :span="12">
<el-form-item label="类别" prop="planCategoryId">
<el-select
v-model="formData.planCategoryId"
placeholder="请选择类别"
v-model="formData.planCategoryId"
clearable
>
<el-option
v-for="item in planCategoryOptions"
:key="item.planMajorId"
:label="item.planMajorName"
:value="item.planMajorId"
:label="item.planMajorName"
v-for="item in planCategoryOptions"
/>
</el-select>
</el-form-item>
@ -109,9 +109,13 @@
<template v-for="(item, index) in formData.workloadList" :key="index">
<el-col :span="8">
<el-form-item
:label="index == 0 ? '工作量' : ''"
:prop="`workloadList.${index}.workloadCategoryId`"
:class="{ 'hide-required': index !== 0 }"
>
<template #label>
<span v-if="index == 0">工作量</span>
<span v-else style="visibility: hidden">工作量</span>
</template>
<el-select
clearable
placeholder="请选择工作量类别"
@ -136,6 +140,8 @@
clearable
style="width: 240px"
placeholder="输入数量"
show-word-limit
maxlength="7"
v-model="item.workloadNum"
>
<template #suffix>
@ -165,6 +171,8 @@
<el-form-item label="基塔数" prop="towerBaseNumber">
<el-input
clearable
show-word-limit
maxlength="7"
placeholder="请输入基塔数"
v-model.trim="formData.towerBaseNumber"
/>
@ -201,7 +209,21 @@
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="计划投入管理人员" prop="planPersonnelList">
<el-form-item prop="planPersonnelList">
<template #label>
<div style="display: flex; flex-direction: column">
<span>计划投入管理人员</span>
<span
style="
font-size: 12px;
color: #999;
transform: translateY(-20px);
"
>
(职工劳务派遣海峡)
</span>
</div>
</template>
<el-input
readonly
placeholder="点击选择管理人员"
@ -223,6 +245,8 @@
<el-form-item label="计划投入管理车辆数" prop="planCarNum">
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="请输入车辆数量"
v-model.trim="formData.planCarNum"
/>
@ -232,10 +256,25 @@
<el-row :gutter="24">
<el-col :span="7">
<el-form-item label="计划投入熟练工人员数量" prop="planSkilledWorkerNum">
<el-form-item prop="planSkilledWorkerNum">
<template #label>
<div style="display: flex; flex-direction: column">
<span>计划投入熟练工人员数量</span>
<span
style="
font-size: 12px;
color: #999;
transform: translateY(-20px);
"
>
(分包)
</span>
</div>
</template>
<el-input
clearable
placeholder="请输入熟练工人员数量"
maxlength="7"
show-word-limit
placeholder="熟练工人员数量"
v-model.trim="formData.planSkilledWorkerNum"
/>
</el-form-item>
@ -244,20 +283,37 @@
<el-form-item
label="计划投入工日"
prop="planSkilledWorkerDay"
label-width="auto"
label-width="120"
>
<el-input
clearable
placeholder="计划投入工日"
maxlength="7"
placeholder="工日"
v-model.trim="formData.planSkilledWorkerDay"
/>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item label="计划投入辅助工人员数量" prop="planAuxiliaryWorkerNum">
<el-form-item prop="planAuxiliaryWorkerNum">
<template #label>
<div style="display: flex; flex-direction: column">
<span>计划投入辅助工人员数量</span>
<span
style="
font-size: 12px;
color: #999;
transform: translateY(-20px);
"
>
(分包)
</span>
</div>
</template>
<el-input
clearable
placeholder="请输入辅助工人员数量"
maxlength="7"
show-word-limit
placeholder="辅助工人员数量"
v-model.trim="formData.planAuxiliaryWorkerNum"
/>
</el-form-item>
@ -266,11 +322,13 @@
<el-form-item
label="计划投入工日"
prop="planAuxiliaryWorkerDay"
label-width="auto"
label-width="120"
>
<el-input
clearable
placeholder="计划投入工日"
maxlength="7"
show-word-limit
placeholder="工日"
v-model.trim="formData.planAuxiliaryWorkerDay"
/>
</el-form-item>
@ -282,6 +340,8 @@
<el-form-item label="计划投入分包车辆数" prop="planSubCarNum">
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="请输入分包车辆数量"
v-model.trim="formData.planSubCarNum"
/>
@ -291,6 +351,8 @@
<el-form-item label="实际工作天数">
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="请输入实际工作天数"
v-model.trim="formData.actualWorkingDay"
/>
@ -630,31 +692,62 @@ const getInitFormData = () => ({
const formData = ref(getInitFormData())
const rules = {
planMajorId: [{ required: true, message: '请选择专业', trigger: 'change' }],
businessTypeId: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
planCategoryId: [{ required: true, message: '请选择类别', trigger: 'change' }],
towerBaseNumber: [{ required: true, message: '请输入基塔数', trigger: 'blur' }],
planPersonnelList: [{ required: true, message: '请选择计划投入管理人员', trigger: 'blur' }],
// rules workloadList
const rules = computed(() => {
const baseRules = {
planMajorId: [{ required: true, message: '请选择专业', trigger: 'blur' }],
businessTypeId: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
planCategoryId: [{ required: true, message: '请选择类别', trigger: 'change' }],
towerBaseNumber: [
{ required: true, message: '请输入基塔数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
],
planPersonnelList: [{ required: true, message: '请选择计划投入管理人员', trigger: 'blur' }],
}
planCarNum: [{ required: true, message: '请输入计划投入管理人员车辆数', trigger: 'blur' }],
planSkilledWorkerNum: [
{ required: true, message: '请输入计划投入熟练工人员数量', trigger: 'blur' },
],
// workloadList
// workloadList
baseRules.workloadList = formData.value.workloadList.map(() => ({
workloadCategoryId: [{ required: true, message: '请选择工作量类别', trigger: 'change' }],
workloadNum: [
{ required: true, message: '请输入工作量', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
],
}))
planAuxiliaryWorkerNum: [
{ required: true, message: '请输入计划投入辅助工人员数量', trigger: 'blur' },
],
planSkilledWorkerDay: [
{ required: true, message: '请输入计划投入熟练工人员工日', trigger: 'blur' },
],
planAuxiliaryWorkerDay: [
{ required: true, message: '请输入计划投入辅助工人员工日', trigger: 'blur' },
],
planSubCarNum: [{ required: true, message: '请输入计划投入分包车辆数', trigger: 'blur' }],
actualWorkingDay: [{ required: true, message: '请输入实际工作天数', trigger: 'blur' }],
}
//
baseRules.planCarNum = [
{ required: true, message: '请输入管理人员车辆数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planSkilledWorkerNum = [
{ required: true, message: '请输入熟练工人员数量', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planAuxiliaryWorkerNum = [
{ required: true, message: '请输入辅助工人员数量', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planSkilledWorkerDay = [
{ required: true, message: '请输入工日', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planAuxiliaryWorkerDay = [
{ required: true, message: '请输入工日', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planSubCarNum = [
{ required: true, message: '请输入分包车辆数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.actualWorkingDay = [
{ required: true, message: '请输入实际工作天数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
return baseRules
})
const onBack = () => {
//
@ -722,6 +815,16 @@ const onAddWorkLoad = () => {
const onDeleteWorkLoad = (index) => {
formData.value.workloadList.splice(index, 1)
//
nextTick(() => {
//
formData.value.workloadList.forEach((item, idx) => {
formRef.value?.clearValidate([
`workloadList.${idx}.workloadCategoryId`,
`workloadList.${idx}.workloadNum`,
])
})
})
}
//
@ -870,15 +973,22 @@ const onChangeWorkLoadCategory = (event, index) => {
const workloadCategory = planWorkLoadCategoryOptions.value.find(
(item) => item.workloadCategoryId === workloadCategoryId,
)
formData.value.workloadList[index].unitPrice = workloadCategory.unitPrice
formData.value.workloadList[index].workloadCategoryName = workloadCategory.workloadCategoryName
if (workloadCategory) {
formData.value.workloadList[index].unitPrice = workloadCategory.unitPrice
formData.value.workloadList[index].workloadCategoryName =
workloadCategory.workloadCategoryName
}
//
nextTick(() => {
formRef.value?.validateField(`workloadList.${index}.workloadCategoryId`)
})
}
//
const getDetail = async () => {
const result = await getMonthlyPlanDetailAPI({ monthlyPlanId: route.query.monthlyPlanId })
console.log('result--', result)
if (result.code === 200 && result.data) {
const data = result.data
@ -978,16 +1088,89 @@ onMounted(() => {
<style scoped lang="scss">
.monthly-plan-edit {
.page-header {
margin-bottom: 16px;
margin-bottom: 20px;
padding: 16px 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
:deep(.el-page-header__left) {
.el-page-header__back {
padding: 8px;
border-radius: 6px;
transition: all 0.3s ease;
background-color: transparent;
&:hover {
background-color: #f0f2f5;
color: #1677ff;
transform: translateX(-2px);
}
&::before {
font-size: 16px;
font-weight: 600;
}
}
}
:deep(.el-page-header__content) {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin-left: 12px;
}
}
// 0 required *
:deep(.hide-required) {
&.is-required .el-form-item__label::before {
display: none;
}
}
.card-section {
margin-bottom: 16px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
:deep(.el-card__header) {
padding: 16px 20px;
background-color: #fafbfc;
border-bottom: 1px solid #f0f2f5;
}
:deep(.el-card__body) {
padding: 24px 20px;
}
}
.card-header {
font-weight: 600;
font-size: 14px;
font-size: 15px;
color: #1f2937;
position: relative;
padding-left: 12px;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
border-radius: 2px;
}
}
.page-footer {

View File

@ -68,6 +68,8 @@ import {
delMonthlyPlanAPI,
} from '@/api/planMange/monthlyPlan.js'
import { getInspectionStationSelectAPI, getMajorTypeCategorySelectAPI } from '@/api/common.js'
import useDictStore from '@/store/modules/dict'
import { selectDictLabel } from '@/utils/ruoyi'
import config from './config'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
@ -75,9 +77,10 @@ import ComDialog from '@/components/ComDialog/index.vue'
import AddForm from './addForm.vue'
const { proxy } = getCurrentInstance()
const dictStore = useDictStore()
const { plan_risk_level } = proxy.useDict('plan_risk_level')
const router = useRouter()
const { tableColumns, dialogConfig, buildFormColumns } = config
const { tableColumns: baseTableColumns, dialogConfig, buildFormColumns } = config
const comTableRef = ref(null)
const addFormRef = ref(null)
@ -116,6 +119,30 @@ const formColumns = computed(() =>
),
)
// formatter store
const createDynamicDictFormatter = (dictType, fieldName = null) => {
return (row, column, cellValue) => {
const value = fieldName ? row[fieldName] : cellValue
// store
const dictData = dictStore.getDict(dictType) || []
return selectDictLabel(dictData, value)
}
}
// formatter
const tableColumns = computed(() => {
return baseTableColumns.map((column) => {
if (column.prop === 'riskLevel') {
return {
...column,
// 使 formatter
formatter: createDynamicDictFormatter('plan_risk_level', 'riskLevel'),
}
}
return column
})
})
//
const actionColumns = [
{
@ -199,7 +226,7 @@ const onHandleCancel = () => {
//
const onHandleSave = async () => {
addFormRef.value.validate()
await addFormRef.value.validate()
const paramsList = addFormRef.value.getSelectedTasks().map((item) => ({
planManagementId: item.planManagementId,
@ -235,11 +262,6 @@ const onExportPersonArrange = (queryParams) => {
...queryParams,
},
`人员安排表.xlsx`,
/*{
headers: {
'Content-Type': 'application/json',
},
},*/
)
}
@ -251,11 +273,6 @@ const onExportOverallSummary = (queryParams) => {
...queryParams,
},
`整体汇总表.xlsx`,
{
headers: {
'Content-Type': 'application/json',
},
},
)
}
@ -267,11 +284,6 @@ const onExportWorkloadSummary = (queryParams) => {
...queryParams,
},
`工作量汇总表.xlsx`,
{
headers: {
'Content-Type': 'application/json',
},
},
)
}
</script>

View File

@ -10,11 +10,12 @@
<!-- 月份 -->
<el-form-item label="月份" prop="planManagementMonth">
<el-date-picker
v-model="formData.planManagementMonth"
type="month"
style="width: 100%"
value-format="YYYY-MM"
placeholder="请选择月份"
style="width: 100%"
v-model="formData.planManagementMonth"
@change="handleMonthChange"
/>
</el-form-item>
@ -22,7 +23,7 @@
<el-form-item label="项目名称" prop="projectName">
<el-input
clearable
maxlength="50"
maxlength="100"
show-word-limit
placeholder="请输入项目名称"
v-model.trim="formData.projectName"
@ -35,9 +36,9 @@
type="textarea"
maxlength="500"
show-word-limit
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入作业内容"
v-model.trim="formData.workContent"
:autosize="{ minRows: 4, maxRows: 8 }"
/>
</el-form-item>
@ -64,20 +65,28 @@
<el-date-picker
v-model="formData.stareDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择计划开始时间"
style="width: 100%"
value-format="YYYY-MM-DD"
:placeholder="!formData.planManagementMonth ? '请先选择月份' : '请选择计划开始时间'"
:disabled="!formData.planManagementMonth"
:disabled-date="disabledStartDate"
:default-value="defaultDateValue"
@change="handleStartDateChange"
/>
</el-form-item>
<!-- 计划结束时间 -->
<el-form-item label="计划结束时间" prop="endDate">
<el-date-picker
v-model="formData.endDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择计划结束时间"
style="width: 100%"
v-model="formData.endDate"
value-format="YYYY-MM-DD"
:placeholder="!formData.planManagementMonth ? '请先选择月份' : '请选择计划结束时间'"
:disabled="!formData.planManagementMonth"
:disabled-date="disabledEndDate"
:default-value="defaultDateValue"
@change="handleEndDateChange"
/>
</el-form-item>
@ -104,7 +113,7 @@
type="textarea"
maxlength="200"
show-word-limit
:autosize="{ minRows: 3, maxRows: 6 }"
:autosize="{ minRows: 4, maxRows: 8 }"
placeholder="请输入备注"
v-model.trim="formData.remark"
/>
@ -113,7 +122,7 @@
</template>
<script setup name="AddAndEditForm">
import { ref } from 'vue'
import { ref, watch, computed } from 'vue'
const { proxy } = getCurrentInstance()
const { plan_risk_level } = proxy.useDict('plan_risk_level')
@ -130,13 +139,186 @@ const props = defineProps({
const formRef = ref(null)
//
const monthRange = computed(() => {
if (!props.formData.planManagementMonth) {
return { start: null, end: null }
}
const [year, month] = props.formData.planManagementMonth.split('-')
const start = new Date(year, parseInt(month) - 1, 1)
const end = new Date(year, parseInt(month), 0, 23, 59, 59) //
return {
start: start.getTime(),
end: end.getTime(),
}
})
//
const defaultDateValue = computed(() => {
if (!props.formData.planManagementMonth) {
return new Date() // 使
}
const [year, month] = props.formData.planManagementMonth.split('-')
//
return new Date(year, parseInt(month) - 1, 1)
})
//
const isDateInMonth = (dateStr) => {
if (!dateStr || !monthRange.value.start) return false
const date = new Date(dateStr)
const timestamp = date.getTime()
return timestamp >= monthRange.value.start && timestamp <= monthRange.value.end
}
//
const handleMonthChange = (value) => {
// value YYYY-MM
if (!value) {
//
props.formData.stareDate = ''
props.formData.endDate = ''
formRef.value?.clearValidate(['stareDate', 'endDate'])
return
}
//
const [year, month] = value.split('-')
const monthStart = new Date(year, parseInt(month) - 1, 1).getTime()
const monthEnd = new Date(year, parseInt(month), 0, 23, 59, 59).getTime()
//
if (props.formData.stareDate) {
const stareTimestamp = new Date(props.formData.stareDate).getTime()
if (stareTimestamp < monthStart || stareTimestamp > monthEnd) {
props.formData.stareDate = ''
formRef.value?.clearValidate('stareDate')
}
}
//
if (props.formData.endDate) {
const endTimestamp = new Date(props.formData.endDate).getTime()
if (endTimestamp < monthStart || endTimestamp > monthEnd) {
props.formData.endDate = ''
formRef.value?.clearValidate('endDate')
}
}
}
//
const disabledStartDate = (time) => {
if (!monthRange.value.start || !monthRange.value.end) {
return true
}
const timestamp = time.getTime()
return timestamp < monthRange.value.start || timestamp > monthRange.value.end
}
//
const disabledEndDate = (time) => {
if (!monthRange.value.start || !monthRange.value.end) {
return true
}
const timestamp = time.getTime()
//
if (timestamp < monthRange.value.start || timestamp > monthRange.value.end) {
return true
}
//
if (props.formData.stareDate) {
const startTimestamp = new Date(props.formData.stareDate).getTime()
return timestamp < startTimestamp
}
return false
}
//
const handleStartDateChange = (value) => {
//
if (value && props.formData.endDate) {
const startTimestamp = new Date(value).getTime()
const endTimestamp = new Date(props.formData.endDate).getTime()
if (endTimestamp < startTimestamp) {
props.formData.endDate = ''
formRef.value?.clearValidate('endDate')
}
}
}
//
const handleEndDateChange = (value) => {
//
if (value && props.formData.stareDate) {
const startTimestamp = new Date(props.formData.stareDate).getTime()
const endTimestamp = new Date(value).getTime()
if (endTimestamp < startTimestamp) {
proxy.$modal.msgError('结束时间不能早于开始时间')
props.formData.endDate = ''
formRef.value?.clearValidate('endDate')
}
}
}
const rules = {
planManagementMonth: [{ required: true, message: '请选择月份', trigger: 'change' }],
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
workContent: [{ required: true, message: '请输入作业内容', trigger: 'blur' }],
inspectionStationId: [{ required: true, message: '请选择实施部门', trigger: 'change' }],
stareDate: [{ required: true, message: '请选择计划开始时间', trigger: 'change' }],
endDate: [{ required: true, message: '请选择计划结束时间', trigger: 'change' }],
stareDate: [
{ required: true, message: '请选择计划开始时间', trigger: 'change' },
{
validator: (rule, value, callback) => {
if (!value) {
callback()
return
}
if (!props.formData.planManagementMonth) {
callback(new Error('请先选择月份'))
return
}
if (!isDateInMonth(value)) {
callback(new Error('开始时间必须在选定的月份内'))
return
}
callback()
},
trigger: 'change',
},
],
endDate: [
{ required: true, message: '请选择计划结束时间', trigger: 'change' },
{
validator: (rule, value, callback) => {
if (!value) {
callback()
return
}
if (!props.formData.planManagementMonth) {
callback(new Error('请先选择月份'))
return
}
if (!isDateInMonth(value)) {
callback(new Error('结束时间必须在选定的月份内'))
return
}
if (props.formData.stareDate) {
const startTimestamp = new Date(props.formData.stareDate).getTime()
const endTimestamp = new Date(value).getTime()
if (endTimestamp < startTimestamp) {
callback(new Error('结束时间不能早于开始时间'))
return
}
}
callback()
},
trigger: 'change',
},
],
riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
}

View File

@ -41,7 +41,9 @@ import { ref, nextTick, getCurrentInstance, computed } from 'vue'
import { listPlanAPI, addPlanAPI, updatePlanAPI, delPlanAPI } from '@/api/planMange/plan.js'
import { getInspectionStationSelectAPI } from '@/api/common.js'
import { useOptions } from '@/hooks/useOptions'
import useDictStore from '@/store/modules/dict'
import { selectDictLabel } from '@/utils/ruoyi'
import dayjs from 'dayjs'
import config from './config'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
@ -50,6 +52,7 @@ import AddAndEditForm from './addAndEditForm.vue'
const { tableColumns: baseTableColumns, dialogConfig, buildFormColumns } = config
const { proxy } = getCurrentInstance()
const dictStore = useDictStore()
const formRef = ref(null)
const comTableRef = ref(null)
@ -63,13 +66,24 @@ const formColumns = computed(() =>
buildFormColumns(inspectionStationOptions.value, plan_risk_level.value),
)
// formatter store
const createDynamicDictFormatter = (dictType, fieldName = null) => {
return (row, column, cellValue) => {
const value = fieldName ? row[fieldName] : cellValue
// store
const dictData = dictStore.getDict(dictType) || []
return selectDictLabel(dictData, value)
}
}
// formatter
const tableColumns = computed(() => {
return baseTableColumns.map((column) => {
if (column.prop === 'riskLevel') {
return {
...column,
formatter: proxy.createDictFormatter(plan_risk_level.value, 'riskLevel'),
// 使 formatter
formatter: createDynamicDictFormatter('plan_risk_level', 'riskLevel'),
}
}
return column
@ -87,7 +101,7 @@ console.log(inspectionStationOptions, 'inspectionStationOptions')
// 1.
const getInitFormData = () => ({
planManagementMonth: '', //
planManagementMonth: dayjs().format('YYYY-MM'), //
projectName: '', //
workContent: '', //
inspectionStationId: null, //

View File

@ -3,8 +3,8 @@ import path from 'path'
import createVitePlugins from './vite/plugins'
// const baseUrl = 'http://192.168.0.133:58080' // 后端接口地址 超子
// const baseUrl = 'http://localhost:58080' // 后端接口地址
const baseUrl = 'http://192.168.0.60:58080' // 后端接口地址 福海
// const baseUrl = 'http://localhost:58062' // 后端接口地址
const baseUrl = 'http://192.168.0.60:58062' // 后端接口地址 福海
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
@ -16,7 +16,7 @@ export default defineConfig(({ mode, command }) => {
// 部署生产环境和开发环境下的URL。
// 默认情况下vite 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
base: VITE_APP_ENV === 'production' ? '/' : '/',
base: VITE_APP_ENV === 'production' ? '/ynDigitalGadgets' : '/',
plugins: createVitePlugins(env, command === 'build'),
resolve: {
// https://cn.vitejs.dev/config/#resolve-alias