静态部分完成70%

main
wangjy 2024-10-09 17:46:13 +08:00
parent 63baa08e8c
commit 60cb68401f
16 changed files with 2810 additions and 425 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,18 +1,5 @@
@use 'sass:math';
// @font-face {
// font-family: AliPuHui;
// /**/
// src: url('../font/Alibaba-PuHuiTi/Alibaba-PuHuiTi-Regular.ttf');
// /**/
// }
// @font-face {
// font-family: AliPuHuiBold;
// /**/
// src: url('../font/Alibaba-PuHuiTi/Alibaba-PuHuiTi-Bold.ttf');
// /**/
// }
* {
box-sizing: border-box;
@ -117,76 +104,11 @@ body {
.aifs {
align-items: flex-start;
}
//
.pd8-15 {
padding: 0.08rem 0.15rem;
}
.pd8-23 {
padding: 0.08rem 0.23rem;
}
.w300-h30 {
width: 70%;
height: 32px;
flex-shrink: 0;
flex-grow: 0;
}
:deep(.w300-h30) {
width: 70%;
height: 32px;
flex-shrink: 0;
flex-grow: 0;
}
.h100 {
height: 100px;
}
.h200 {
height: 200px;
}
.h300 {
height: 300px;
}
.w100-h30 {
width: 1rem;
height: 30px;
}
.w200-h30 {
width: 70% !important;
height: 30px;
flex-shrink: 0;
flex-grow: 0;
}
:deep(.w200-h30) {
width: 70% !important;
height: 30px;
flex-shrink: 0;
flex-grow: 0;
}
.w200-h20 {
width: 2rem;
height: 20px;
}
.w140-h30 {
width: 1.4rem;
height: 30px;
}
.w150-h30 {
width: 150px;
height: 30px;
}
.w90-h40 {
width: 90px;
height: 40px;
}
.w75-h30 {
width: 75px;
height: 30px;
}
//
.ml10 {
@ -256,18 +178,8 @@ body {
.no_wrap {
white-space: nowrap;
}
.no-data {
// color: #000;
width: 100%;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
// .theme-color {
// color: $lightThemColor;
// }
/* 竖向弹性盒子 */
.flex-col {
@include flex-row-vc;
@ -351,14 +263,6 @@ body {
}
}
@mixin scroll-none {
scrollbar-width: none;
/* firefox */
-ms-overflow-style: none;
/* IE 10+ */
overflow-x: hidden;
overflow-y: scroll;
}
// dialogbody,
.el-popup-parent--hidden {
@ -412,67 +316,8 @@ body {
// color: #ffffff;
// }
}
// dialog
.center-dialog {
display: flex;
justify-content: center;
align-items: center;
// overflow: hidden;
:deep(.el-dialog) {
margin: 0 !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.el-dialog__header {
border-bottom: 1px solid #d8d8d8;
}
.el-dialog__body {
// overflow: hidden;
overflow: auto;
// height: 70vh; //90%
}
}
}
.common-dialog {
display: flex;
justify-content: center;
align-items: center;
// overflow: hidden;
:deep(.el-dialog) {
margin: 0 !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.el-dialog__header {
border-bottom: 1px solid #d8d8d8;
height: 38px;
font-weight: bold;
font-size: 14px;
line-height: 38px;
padding: 0;
padding-left: 16px;
text-align: left;
.el-dialog__title {
color: #333333;
font-size: 14px !important;
}
}
.el-dialog__body {
// overflow: hidden;
overflow: auto;
max-height: 90vh; //90%
// height: auto;
}
.el-dialog__headerbtn {
height: 36px;
}
.table-container {
min-height: 30vh;
}
}
}
:deep(.el-form-item__content) {
align-items: flex-start !important;
}
@ -491,35 +336,8 @@ body {
.pr15 {
padding-right: 15px;
}
.image-slot-error {
height: 100%;
width: 100%;
background-color: #f7f5f5;
align-items: center;
justify-content: center;
display: flex;
}
.no-data {
// color: #000;
width: 100%;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
// 线
.text_underline {
font-family: MicrosoftYaHei;
font-size: 14px;
color: #5584ff;
line-height: 22px;
text-align: center;
font-style: normal;
text-decoration-line: underline;
-moz-text-decoration-line: underline;
cursor: pointer;
}
:deep(.el-button--primary:not(.is-text)) {
background-color: #5584ff !important ;
color: #fff !important;
@ -531,18 +349,7 @@ body {
padding: 10px 0;
background: #f5f5f5;
}
.optionTitle {
background-color: #f7f7f7;
height: 40px;
line-height: 40px;
padding-left: 10px;
}
// :deep(.el-radio-button__inner){
// width: 120px;
// }
// :deep(*:not(.el-dialog__body) .is-active .el-radio-button__inner){
// background: #5584FF;
// }
:deep(.el-dialog--center .el-dialog__body) {
padding: 32px !important;
}

View File

@ -0,0 +1,95 @@
/* 适用于所有页面的通用样式 */
:root {
--primary-color: #1890ff;
--secondary-color: #f0f2f5;
--text-color: #333333;
--border-color: #e8e8e8;
}
.page-container {
background-color: var(--secondary-color);
padding: 24px;
min-height: 100vh;
}
.content-card {
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 24px;
}
.page-title {
font-size: 20px;
font-weight: 500;
color: var(--text-color);
margin-bottom: 24px;
}
/* 表格样式优化 */
:deep(.el-table) {
border: 1px solid var(--border-color);
border-radius: 4px;
}
:deep(.el-table__header) {
background-color: #fafafa;
}
:deep(.el-table__header th) {
background-color: #fafafa;
color: var(--text-color);
font-weight: 500;
}
:deep(.el-table__body td) {
color: var(--text-color);
}
/* 按钮样式优化 */
.el-button {
border-radius: 4px;
}
.el-button--primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
/* 表单样式优化 */
.el-form-item__label {
color: var(--text-color);
}
.el-input__wrapper,
.el-select .el-input__wrapper {
border-radius: 4px;
}
/* 对话框样式优化 */
:deep(.el-dialog) {
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
:deep(.el-dialog__header) {
background-color: #fafafa;
padding: 16px 24px;
border-bottom: 1px solid var(--border-color);
}
:deep(.el-dialog__title) {
font-size: 16px;
font-weight: 500;
color: var(--text-color);
}
:deep(.el-dialog__body) {
padding: 24px;
}
:deep(.el-dialog__footer) {
padding: 16px 24px;
border-top: 1px solid var(--border-color);
}

View File

@ -0,0 +1,254 @@
<template>
<el-dialog title="选择人员" :modelValue="dialogVisible" width="50%" @close="handleClose">
<div class="select-user-container">
<div class="org-tree">
<el-tree :data="treeData" :props="defaultProps" @node-click="handleNodeClick" default-expand-all />
</div>
<div class="user-list">
<CustomTable
ref="customTableRef"
:columns="columns"
:tableData="userData"
:total="total"
:show-selection="true"
:show-index="true"
:table-height="tableHeight"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #operation="{ row }">
<div class="operation-buttons">
<el-button text type="primary" @click="handleEdit(row)"></el-button>
<el-button text type="primary" @click="showTimesheet(row)"></el-button>
<el-button text type="danger" @click="handleDelete(row)"></el-button>
</div>
</template>
</CustomTable>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose"></el-button>
<el-button type="primary" @click="handleConfirm"></el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
import CustomTable from '@/components/table.vue'
const props = defineProps({
multiSelect: {
type: Boolean,
default: true,
},
dialogVisible: {
type: Boolean,
required: true,
},
currentSelectedUser: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['update:dialogVisible', 'close', 'confirm'])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(100)
const selectedUsers = ref([])
const currentDepartment = ref('')
const tableHeight = ref(350) //
//
const treeData = [
{
label: '运维团队',
children: [{ label: '外场人员' }, { label: '内场人员' }, { label: '外场临时人员' }],
},
]
const defaultProps = {
children: 'children',
label: 'label',
}
//
const columns = [
{ prop: 'name', label: '姓名' },
{ prop: 'department', label: '部门' },
{ prop: 'role', label: '角色' },
]
//
const allUserData = reactive(
Array(100)
.fill(null)
.map((_, index) => ({
id: index + 1,
name: `张三${index + 1}`,
department: ['外场人员', '内场人员', '外场临时人员'][index % 3],
role: '客户经理',
})),
)
const userData = computed(() => {
let filteredData = allUserData
if (currentDepartment.value) {
filteredData = allUserData.filter(user => user.department === currentDepartment.value)
}
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredData.slice(start, end)
})
const handleNodeClick = data => {
currentDepartment.value = data.label
currentPage.value = 1
fetchUserList()
}
const handleCurrentChange = val => {
currentPage.value = val
fetchUserList()
}
const handleSizeChange = val => {
pageSize.value = val
fetchUserList()
}
const handleClose = () => {
emit('update:dialogVisible', false)
emit('close')
}
const handleConfirm = () => {
emit('confirm', selectedUsers.value)
handleClose()
}
const customTableRef = ref(null)
const isInternalChange = ref(false)
const handleSelectionChange = val => {
if (isInternalChange.value) return
if (!props.multiSelect) {
isInternalChange.value = true
nextTick(() => {
if (val.length > 0) {
const lastSelected = val[val.length - 1]
selectedUsers.value = [lastSelected]
customTableRef.value?.clearSelection()
customTableRef.value?.toggleRowSelection(lastSelected, true)
} else {
selectedUsers.value = []
}
isInternalChange.value = false
})
} else {
selectedUsers.value = val
}
}
// currentSelectedUser
watch(
() => props.currentSelectedUser,
newVal => {
isInternalChange.value = true
nextTick(() => {
selectedUsers.value = newVal
if (customTableRef.value) {
customTableRef.value.clearSelection()
newVal.forEach(user => {
const row = allUserData.find(item => item.id === user.id)
if (row) {
customTableRef.value.toggleRowSelection(row, true)
}
})
}
isInternalChange.value = false
})
},
{ immediate: true, deep: true },
)
//
onMounted(() => {
nextTick(() => {
if (customTableRef.value && props.currentSelectedUser.length > 0) {
customTableRef.value.clearSelection()
props.currentSelectedUser.forEach(user => {
const row = allUserData.find(item => item.id === user.id)
if (row) {
customTableRef.value.toggleRowSelection(row, true)
}
})
}
})
})
const fetchUserList = async () => {
// API
// const response = await api.getUserList({
// page: currentPage.value,
// pageSize: pageSize.value,
// department: currentDepartment.value
// })
// allUserData = response.data.list
// total.value = response.data.total
// 使
total.value = allUserData.filter(
user => !currentDepartment.value || user.department === currentDepartment.value,
).length
}
//
// onMounted(() => {
// fetchUserList()
// })
</script>
<style scoped>
.select-user-container {
display: flex;
height: 400px;
}
.org-tree {
width: 200px;
border-right: 1px solid #dcdfe6;
padding-right: 20px;
overflow-y: auto;
}
.user-list {
flex: 1;
padding-left: 20px;
display: flex;
flex-direction: column;
}
:deep(.el-dialog__footer) {
padding: 20px 20px 20px 0;
}
.dialog-footer {
display: flex;
justify-content: center;
width: 100%;
}
.dialog-footer .el-button {
margin-left: 10px;
}
.dialog-footer .el-button:first-child {
margin-left: 0;
}
</style>

View File

@ -1,144 +1,163 @@
<template>
<div class="custom-table" ref="tableContainer">
<el-table
:data="tableData"
:height="tableHeight"
v-bind="$attrs"
@selection-change="handleSelectionChange"
>
<el-table-column
v-if="showSelection"
type="selection"
width="55"
/>
<el-table-column
v-if="showIndex"
type="index"
width="50"
label="序号"
/>
<template v-for="(column, index) in columns" :key="index">
<el-table-column
v-bind="column"
:prop="column.prop"
:label="column.label"
>
<template #default="scope">
<slot :name="column.prop" :row="scope.row">
<div class="custom-table" ref="tableContainer">
<el-table
ref="elTableRef"
:data="tableData"
:max-height="computedTableHeight"
v-bind="$attrs"
@selection-change="handleSelectionChange"
:border="border"
>
<el-table-column v-if="showSelection" type="selection" width="55" />
<el-table-column v-if="showIndex" type="index" width="50" label="序号" />
<template v-for="(column, index) in columns" :key="index">
<el-table-column v-bind="column" :prop="column.prop" :label="column.label">
<template #default="scope">
<slot :name="column.prop" :row="scope.row">
<template v-if="column.type === 'multiButton'">
<div class="button-group">
<el-button
v-for="(userName, userIndex) in scope.row[column.prop]"
:key="userIndex"
type="text"
size="small"
@click="handleButtonClick(userName, column.prop)"
>
{{ userName }}
</el-button>
</div>
</template>
<template v-else>
{{ scope.row[column.prop] }}
</slot>
</template>
</el-table-column>
</template>
</el-table>
<div class="pagination-container">
<el-pagination
v-if="showPagination"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
:page-sizes="pageSizes"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
</slot>
</template>
</el-table-column>
</template>
</el-table>
<div class="pagination-container" ref="paginationContainer" v-if="showPagination">
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:total="total"
:page-sizes="pageSizes"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import useTableResize from '@/hooks/useTableResize'
const props = defineProps({
columns: {
type: Array,
required: true
},
tableData: {
type: Array,
default: () => []
},
showSelection: {
type: Boolean,
default: false
},
showIndex: {
type: Boolean,
default: false
},
showPagination: {
type: Boolean,
default: true
},
total: {
type: Number,
default: 0
},
pageSizes: {
type: Array,
default: () => [10, 20, 30, 50]
},
tableHeight: {
type: Number,
default: 400
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import useTableResize from '@/hooks/useTableResize'
const props = defineProps({
columns: {
type: Array,
required: true,
},
tableData: {
type: Array,
default: () => [],
},
showSelection: {
type: Boolean,
default: false,
},
showIndex: {
type: Boolean,
default: false,
},
showPagination: {
type: Boolean,
default: true,
},
total: {
type: Number,
default: 0,
},
pageSizes: {
type: Array,
default: () => [10, 20, 30, 50],
},
maxHeight: {
type: Number,
default: null,
},
offsetHeight: {
type: Number,
default: 0,
},
border: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['selection-change', 'size-change', 'current-change', 'button-click'])
const currentPage = ref(1)
const pageSize = ref(10)
const tableContainer = ref(null)
const paginationContainer = ref(null)
const computedTableHeight = ref(null)
const elTableRef = ref(null)
const handleSelectionChange = selection => {
emit('selection-change', selection)
}
const handleSizeChange = val => {
pageSize.value = val
emit('size-change', val)
}
const handleCurrentChange = val => {
currentPage.value = val
emit('current-change', val)
}
const updateTableHeight = () => {
nextTick(() => {
if (tableContainer.value && paginationContainer.value) {
const parentElement = tableContainer.value.parentElement
const parentHeight = parentElement.clientHeight
const tableTop = tableContainer.value.getBoundingClientRect().top - parentElement.getBoundingClientRect().top
const paginationHeight = props.showPagination ? paginationContainer.value.offsetHeight + 30 : 0 // margin-top
computedTableHeight.value = parentHeight - tableTop - paginationHeight
}
})
const emit = defineEmits(['selection-change', 'size-change', 'current-change'])
const currentPage = ref(1)
const pageSize = ref(10)
const tableContainer = ref(null)
const tableHeight = computed(() => props.tableHeight)
const { w: tableWidth } = useTableResize()
const handleSelectionChange = (selection) => {
emit('selection-change', selection)
}
const handleSizeChange = (val) => {
pageSize.value = val
emit('size-change', val)
}
const handleCurrentChange = (val) => {
currentPage.value = val
emit('current-change', val)
}
const updateTableHeight = () => {
nextTick(() => {
if (tableContainer.value) {
const containerRect = tableContainer.value.getBoundingClientRect()
const windowHeight = window.innerHeight
const containerTop = containerRect.top
const paginationHeight = props.showPagination ? 32 : 0 //
tableHeight.value = windowHeight - containerTop - paginationHeight
}
})
}
onMounted(() => {
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
})
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight)
})
</script>
<style scoped>
.custom-table {
width: 100%;
display: flex;
flex-direction: column;
}
.pagination-container {
margin-top: 10px;
display: flex;
justify-content: flex-end;
}
</style>
}
// el-table
defineExpose({
clearSelection: () => elTableRef.value?.clearSelection(),
toggleRowSelection: (row, selected) => elTableRef.value?.toggleRowSelection(row, selected),
setCurrentRow: row => elTableRef.value?.setCurrentRow(row),
//
})
onMounted(() => {
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
})
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight)
})
</script>
<style scoped>
.custom-table {
width: 100%;
display: flex;
flex-direction: column;
}
.pagination-container {
margin-top: 10px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -24,7 +24,7 @@
</el-dropdown>
</div>
</el-header>
<RouteTab></RouteTab>
<!-- <RouteTab></RouteTab> -->
<el-container>
<el-aside :style="{ width: isCollapse ? '64px' : '200px' }"><Aside></Aside></el-aside>
<!-- 主体部分 -->
@ -47,7 +47,7 @@ import editPassWordCom from './components/editPassWordCom.vue'
import { useMenu } from '@/layout/hook/hook.aside.js'
import Aside from './components/Aside.vue'
import MainContent from './components/MainContent.vue'
import RouteTab from './components/routeTab.vue'
// import RouteTab from './components/routeTab.vue'
import { loginApi } from '@/utils/api'
import { removeToken } from '@/utils/auth'
import { useUserStore } from '@/stores/user'

View File

@ -15,6 +15,7 @@ window.$api = $api
import { createPinia } from 'pinia'
// 重置css
import '@/assets/styles/reset.scss'
import '@/assets/styles/pagecss.scss'
// 使用svg图标组件
import 'virtual:svg-icons-register'
// 防止Xss攻击的v-html

View File

@ -5,7 +5,18 @@
* @LastEditTime: 2023-07-07 16:09:00
* @Description :路由配置文件 routes静态路由 asyncRoutes异步路由
*/
import RouterView from './RouterView.vue'
import {
HomeFilled,
Briefcase,
User,
Calendar,
Document,
Setting,
PieChart,
List,
Tickets,
Monitor
} from '@element-plus/icons-vue'
import Login from '@/views/Login/Login.vue'
import layout from '@/layout/Layout.vue'
import { shallowRef } from 'vue'
@ -33,16 +44,26 @@ export const asyncRoutes = [
path: '/project',
name: 'project',
authKey: 'Workorder',
redirect: '/project/detail',
redirect: '/project/list',
meta: { title: '项目管理', imgSrc: 'slider/system' },
component: shallowRef(layout),
children: [
{
path: '/project/list',
name: 'projectList',
authKey: 'Workorder',
meta: {
title: '项目列表',
},
component: () => import('@/views/project/list.vue'),
},
{
path: '/project/detail',
name: 'userWork',
name: 'projectDetail',
authKey: 'Workorder',
meta: {
title: '项目详情',
menuHide: true, // 添加这个属性来隐藏菜单项
},
component: () => import('@/views/project/detail.vue'),
},
@ -75,4 +96,59 @@ export const asyncRoutes = [
// },
],
},
{
path: '/worklog',
name: 'worklog',
authKey: 'Workorder',
meta: { title: '工作日志', imgSrc: 'slider/worklog' }, // 您可能需要为工作日志添加一个图标
component: shallowRef(layout),
children: [
{
path: '/worklog/list',
name: 'worklogList',
authKey: 'Workorder',
meta: {
title: '日志列表',
},
component: () => import('@/views/workLog/list.vue'),
},
// 如果需要其他工作日志相关的子路由,可以在这里添加
],
},
{
path: '/projectBank',
name: 'projectBank',
authKey: 'Workorder',
meta: { title: '项目看板', imgSrc: 'slider/worklog' }, // 您可能需要为工作日志添加一个图标
component: shallowRef(layout),
children: [
{
path: '/projectBank/projectProgress',
name: 'projectProgress',
authKey: 'Workorder',
meta: {
title: '项目执行表',
},
component: () => import('@/views/projectBank/projectProgress.vue'),
},
{
path: '/projectBank/userProject',
name: 'userProject',
authKey: 'Workorder',
meta: {
title: '人员项目表',
},
component: () => import('@/views/projectBank/userProject.vue'),
},
{
path: '/projectBank/projectUser',
name: 'projectUser',
authKey: 'Workorder',
meta: {
title: '项目人员表',
},
component: () => import('@/views/projectBank/projectUser.vue'),
},
],
},
]

View File

@ -246,7 +246,7 @@ const codeShadow = ref(false)
position: relative;
flex: 1;
box-shadow: 0px 10px 20px 0px #2860b5;
background: #3e8bff;
background: #fff;
border-bottom-left-radius: 40px;
background: url('@/assets/imgs/编组 3.png') no-repeat;
background-position: center center;

View File

@ -1,34 +1,32 @@
<template>
<div class="project-management">
<el-form :model="formData" :disabled="!isEditing" label-width="120px">
<el-row>
<el-form :model="formData" :rules="rules" label-width="120px" class="custom-form">
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="项目名称">
<el-select v-model="formData.projectName" placeholder="请选择项目名称" class="full-width">
<el-select v-model="formData.projectName" placeholder="请选择项目名称" class="full-width longInput">
<el-option v-for="item in projectOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-col :span="12">
<el-form-item label="项目编号">
<el-input v-model="formData.projectCode" class="full-width" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-form-item label="项目负责人">
<el-input v-model="formData.projectManager" class="full-width" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="预算人天数">
<el-input-number v-model="formData.budgetDays" :min="0" class="full-width" />
<el-input
v-model="formData.projectManager"
placeholder="选择项目负责人"
readonly
@click="openProjectManagerSelect"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="项目状态">
<el-select v-model="formData.projectStatus" placeholder="请选择项目状态" class="full-width">
<el-option label="进行中" value="ongoing" />
@ -37,12 +35,19 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-form-item label="预计工时">
<el-input type="number" v-model="formData.budgetDays" :min="0" class="full-width" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="项目开始时间">
<el-date-picker v-model="formData.startDate" type="date" placeholder="选择日期" class="full-width" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-form-item label="项目结束时间">
<el-date-picker v-model="formData.endDate" type="date" placeholder="选择日期" class="full-width" />
</el-form-item>
@ -51,7 +56,7 @@
</el-form>
<div class="table-actions">
<el-button type="primary" @click="toggleEdit">{{ isEditing ? '' : '' }}</el-button>
<el-button type="primary" :icon="Plus" @click="editUser"></el-button>
</div>
<CustomTable
@ -60,26 +65,88 @@
:total="total"
:show-selection="false"
:show-index="true"
:table-height="400"
:table-height="tableHeight"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #name="{ row }">
<el-input
v-model="row.name"
placeholder="选择人员"
readonly
@click="openSelectUser(row)"
/>
</template>
<template #role="{ row }">
<el-select
v-model="row.role"
placeholder="请选择职位"
@change="handleRoleChange(row)"
>
<el-option v-for="role in roleOptions" :key="role" :label="role" :value="role" />
</el-select>
</template>
<template #operation="{ row }">
<div class="operation-buttons">
<el-button text @click="handleEdit(row)"></el-button>
<el-button text type="danger" @click="handleDelete(row)"></el-button>
<el-button text type="primary" @click="showTimesheet(row)"></el-button>
<el-button text type="primary" @click="showTimesheet(row)"></el-button>
<el-button text type="danger" @click="confirmDelete(row)"></el-button>
</div>
</template>
</CustomTable>
<!-- 修改 SelectUser 组件 -->
<SelectUser
v-if="showSelectUser"
v-model:dialogVisible="showSelectUser"
:multi-select="false"
:selected-users="currentSelectedUser"
@confirm="handleUserConfirm"
@close="closeSelectUser"
/>
<SelectUser
v-if="showProjectManagerSelect"
v-model:dialogVisible="showProjectManagerSelect"
:multi-select="false"
:selected-users="projectManagerSelectedUser"
@confirm="handleProjectManagerConfirm"
@close="closeProjectManagerSelect"
/>
<!-- 删除确认对话框 -->
<el-dialog
v-model="deleteDialogVisible"
title="确认删除"
width="30%"
>
<div class="delete-confirm">
<img src="@/assets/warning.png" alt="警告" class="warning-icon">
<p>确定要删除该成员吗此操作不可逆</p>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="deleteDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleDelete"></el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import CustomTable from '@/components/table.vue'
import { Plus } from '@element-plus/icons-vue'
import SelectUser from '@/components/SelectUser.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const isEditing = ref(false)
const router = useRouter()
const route = useRoute()
const isEditing = ref(true)
const showSelectUser = ref(false)
const showProjectManagerSelect = ref(false)
const formData = reactive({
projectName: '',
projectCode: '',
@ -87,81 +154,178 @@ const formData = reactive({
budgetDays: 0,
projectStatus: '',
startDate: '',
endDate: ''
endDate: '',
})
const projectOptions = [
{ value: 'project1', label: '项目1' },
{ value: 'project2', label: '项目2' },
{ value: 'project3', label: '项目3' }
]
const roleOptions = ['开发', '设计', '测试', '产品', '项目经理']
const columns = [
{ prop: 'name', label: '人员姓名' },
{ prop: 'role', label: '项目角色' },
{ prop: 'workdays', label: '工时天数' },
{ prop: 'weight', label: '人员权重' },
{
prop: 'operation',
label: '操作',
width: 'auto',
{ prop: 'name', label: '人员姓名', slot: 'name' },
{ prop: 'role', label: '项目职位', slot: 'role' },
{ prop: 'workdays', label: '累计人天数' },
{
prop: 'operation',
label: '操作',
width: '200',
fixed: 'right',
className: 'operation-column'
}
className: 'operation-column',
},
]
const tableData = ref(Array(22).fill(null).map((_, index) => ({
name: `员工${index + 1}`,
role: ['开发', '设计', '测试', '产品'][index % 4],
workdays: Math.floor(Math.random() * 20) + 10,
weight: (Math.random() * 0.5 + 0.5).toFixed(2)
})))
const tableData = ref([])
const total = ref(0)
const currentEditingRow = ref(null)
const currentSelectedUser = ref([])
const projectManagerSelectedUser = ref([])
const total = ref(tableData.value.length)
const toggleEdit = () => {
isEditing.value = !isEditing.value
if (!isEditing.value) {
//
console.log('保存表单数据:', formData)
const validateDates = (rule, value, callback) => {
if (formData.startDate && formData.endDate) {
if (new Date(formData.startDate) > new Date(formData.endDate)) {
callback(new Error('开始时间不能大于结束时间'))
} else {
callback()
}
} else {
callback()
}
}
const handleSizeChange = (val) => {
const rules = {
startDate: [
{ required: true, message: '请选择开始时间', trigger: 'change' },
{ validator: validateDates, trigger: 'change' }
],
endDate: [
{ required: true, message: '请选择结束时间', trigger: 'change' },
{ validator: validateDates, trigger: 'change' }
]
}
const editUser = () => {
const newUser = {
id: Date.now(),
name: '',
role: '',
workdays: 0,
userId: null
}
tableData.value.push(newUser)
total.value = tableData.value.length
}
const openSelectUser = (row) => {
currentEditingRow.value = row
currentSelectedUser.value = row.userId ? [{ id: row.userId, name: row.name }] : []
showSelectUser.value = true
}
const handleUserConfirm = (users) => {
if (users.length > 0) {
const selectedUser = users[0]
currentEditingRow.value.name = selectedUser.name
currentEditingRow.value.userId = selectedUser.id
} else {
currentEditingRow.value.name = ''
currentEditingRow.value.userId = null
}
showSelectUser.value = false
}
const closeSelectUser = () => {
showSelectUser.value = false
currentEditingRow.value = null
currentSelectedUser.value = []
}
const openProjectManagerSelect = () => {
projectManagerSelectedUser.value = formData.projectManager
? [{ id: formData.projectManagerId, name: formData.projectManager }]
: []
showProjectManagerSelect.value = true
}
const handleProjectManagerConfirm = (users) => {
if (users.length > 0) {
const selectedUser = users[0]
formData.projectManager = selectedUser.name
formData.projectManagerId = selectedUser.id
} else {
formData.projectManager = ''
formData.projectManagerId = null
}
showProjectManagerSelect.value = false
}
const closeProjectManagerSelect = () => {
showProjectManagerSelect.value = false
}
const handleSizeChange = val => {
console.log('每页显示条数:', val)
}
const handleCurrentChange = (val) => {
const handleCurrentChange = val => {
console.log('当前页:', val)
}
const handleEdit = (row) => {
console.log('编辑行:', row)
}
const handleDelete = (row) => {
console.log('删除行:', row)
const handleRoleChange = (row) => {
}
const showTimesheet = (row) => {
console.log('显示工时表:', row)
router.push({
name: 'workLog',
params: { userId: row.userId },
query: { userName: row.name }
})
}
//
const tableHeight = ref(400) //
const deleteDialogVisible = ref(false)
const userToDelete = ref(null)
const confirmDelete = (row) => {
userToDelete.value = row
deleteDialogVisible.value = true
}
const handleDelete = () => {
const index = tableData.value.findIndex(item => item.id === userToDelete.value.id)
if (index !== -1) {
tableData.value.splice(index, 1)
total.value = tableData.value.length
ElMessage.success('已删除该成员')
deleteDialogVisible.value = false
userToDelete.value = null
}
}
const tableHeight = ref(400)
onMounted(() => {
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
const projectData = route.params.projectData || route.query.projectData
if (projectData) {
Object.assign(formData, projectData)
} else if (route.query.id) {
fetchProjectData(route.query.id)
}
})
const updateTableHeight = () => {
const windowHeight = window.innerHeight
const topOffset = document.querySelector('.project-management').offsetTop
const formHeight = document.querySelector('.el-form').offsetHeight
const actionsHeight = document.querySelector('.table-actions').offsetHeight
const padding = 40 // 20px
tableHeight.value = windowHeight - topOffset - formHeight - actionsHeight - padding
const fetchProjectData = async id => {
try {
const projectData = {
id: id,
projectName: `项目${id}`,
projectCode: `XM00${id}`,
projectManager: '张三',
budgetDays: 30,
projectStatus: 'ongoing',
startDate: '2024-08-31',
endDate: '2024-09-30',
}
Object.assign(formData, projectData)
} catch (error) {
console.error('获取项目数据失败', error)
}
}
</script>
@ -169,38 +333,118 @@ const updateTableHeight = () => {
.project-management {
padding: 20px;
background-color: white;
min-height: 100vh;
height: 88vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
}
.table-actions {
.custom-form {
width: 80%;
}
.form-container {
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
margin-bottom: 20px;
}
.full-width {
.custom-form :deep(.el-form-item) {
margin-bottom: 25px; /* 增加表单行间距 */
}
.custom-form :deep(.el-form-item__content) {
margin-left: auto !important;
width: 80%;
}
.custom-form :deep(.el-input) {
height: 42px; /* 调高输入框高度 */
}
:deep(.el-form-item__label) {
height: 42px; /* 调高输入框高度 */
line-height: 42px;
}
.custom-form :deep(.el-input__wrapper),
.custom-form :deep(.el-date-editor.el-input),
.custom-form :deep(.el-input-number) {
height: 42px; /* 调高输入框高度 */
}
.custom-form :deep(.el-select) {
width: 100%;
}
:deep(.el-input-number) {
width: 100%;
.custom-form :deep(.el-input__wrapper),
.custom-form :deep(.el-select .el-input__wrapper),
.custom-form :deep(.el-date-editor.el-input .el-input__wrapper),
.custom-form :deep(.el-input-number .el-input__wrapper) {
height: 100%;
}
:deep(.el-input-number .el-input__wrapper) {
.content-container {
width: 100%;
display: flex;
flex-direction: column;
}
.table-actions {
margin-bottom: 20px;
align-self: flex-start;
}
:deep(.el-table) {
flex: 1;
overflow: auto;
width: 100%;
}
.operation-buttons {
display: flex;
justify-content: flex-start;
align-items: center;
}
:deep(.operation-buttons .el-button) {
padding: 4px 8px;
margin: 0 2px;
}
:deep(.operation-column) {
background-color: #fff;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
}
/* 添加以下样式以使日期选择器宽度一致 */
:deep(.el-date-editor.el-input) {
width: 100%;
}
:deep(.el-date-editor.el-input .el-input__wrapper) {
width: 100%;
}
:deep(.longInput) {
width: 100% !important;
}
.el-button.is-text {
min-width: 32px !important;
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-select .el-input__wrapper) {
height: 32px;
}
.delete-confirm {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.warning-icon {
width: 64px;
height: 64px;
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,281 @@
<template>
<div class="project-list">
<div class="search-bar">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="项目名称" class="form-item">
<el-input v-model="searchForm.projectName" placeholder="项目名称" />
</el-form-item>
<el-form-item label="负责人" class="form-item">
<el-select v-model="searchForm.manager" placeholder="负责人">
<el-option label="张三" value="张三" />
<el-option label="李四" value="李四" />
</el-select>
</el-form-item>
<el-form-item label="项目状态" class="form-item">
<el-select v-model="searchForm.status" placeholder="项目状态">
<el-option label="待启动" value="待启动" />
<el-option label="进行中" value="进行中" />
<el-option label="已完成" value="已完成" />
</el-select>
</el-form-item>
<el-form-item class="search-buttons">
<el-button type="primary" @click="onSearch"></el-button>
<el-button @click="onReset"></el-button>
</el-form-item>
</el-form>
</div>
<div class="table-actions mb10">
<el-button type="primary" @click="addProject">+ </el-button>
</div>
<CustomTable
:columns="columns"
:tableData="tableData"
:total="total"
:show-selection="false"
:show-index="true"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #operation="{ row }">
<div class="operation-buttons">
<el-button text type="primary" @click="handleEdit(row)"></el-button>
<el-button text type="danger" @click="handleDelete(row)"></el-button>
</div>
</template>
</CustomTable>
<el-dialog
v-model="deleteDialogVisible"
title="删除项目"
width="30%"
:before-close="handleCloseDeleteDialog"
class="delete-dialog"
>
<div class="delete-content">
<el-icon class="warning-icon"><WarningFilled /></el-icon>
<span>删除项目时将删除相关工作日志此操作不可逆请慎重考虑</span>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="deleteDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmDelete"></el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import CustomTable from '@/components/table.vue'
import { WarningFilled } from '@element-plus/icons-vue'
const router = useRouter()
const searchForm = reactive({
projectName: '',
manager: '',
status: '',
})
const columns = [
{ prop: 'projectName', label: '项目名称' },
{ prop: 'projectCode', label: '项目编号' },
{ prop: 'manager', label: '负责人' },
{ prop: 'budgetDays', label: '预计工时(天)' },
{ prop: 'startDate', label: '开始时间' },
{ prop: 'endDate', label: '结束时间' },
{ prop: 'status', label: '项目状态' },
{ prop: 'memberCount', label: '参与项目人数' },
{ prop: 'createdBy', label: '项目创建人' },
{
prop: 'operation',
label: '操作',
width: '150',
fixed: 'right',
className: 'operation-column',
},
]
const tableData = ref([])
const total = ref(0)
const deleteDialogVisible = ref(false)
const currentDeleteItem = ref(null)
const onSearch = () => {
//
console.log('Search with:', searchForm)
fetchProjectList()
}
const onReset = () => {
Object.keys(searchForm).forEach(key => {
searchForm[key] = ''
})
fetchProjectList()
}
const addProject = () => {
router.push('/project/detail')
}
const handleEdit = row => {
router.push({
path: '/project/detail',
query: { id: row.id },
state: { projectData: row },
})
}
const handleDelete = row => {
currentDeleteItem.value = row
deleteDialogVisible.value = true
}
const handleCloseDeleteDialog = () => {
deleteDialogVisible.value = false
currentDeleteItem.value = null
}
const confirmDelete = () => {
//
console.log('Delete project:', currentDeleteItem.value)
deleteDialogVisible.value = false
currentDeleteItem.value = null
fetchProjectList()
}
const handleSizeChange = val => {
console.log(`每页 ${val}`)
fetchProjectList()
}
const handleCurrentChange = val => {
console.log(`当前页: ${val}`)
fetchProjectList()
}
const fetchProjectList = () => {
// 使API
const fakeData = Array(20)
.fill(null)
.map((_, index) => ({
id: index + 1,
projectName: `项目${index + 1}`,
projectCode: `XM00${index + 1}`,
manager: ['张三', '李四', '王五'][index % 3],
budgetDays: Math.floor(Math.random() * 100) + 10,
startDate: '2024-08-31',
endDate: '2024-09-15',
status: ['待启动', '进行中', '已完成'][index % 3],
memberCount: Math.floor(Math.random() * 10) + 3,
createdBy: '张三',
}))
tableData.value = fakeData
total.value = fakeData.length
}
onMounted(() => {
fetchProjectList()
})
</script>
<style scoped>
.project-list {
padding: 20px;
background-color: white;
height: 88vh;
box-sizing: border-box;
overflow: hidden;
}
.search-bar {
margin-bottom: 20px;
}
.demo-form-inline {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
}
.demo-form-inline .el-form-item {
margin-right: 50px; /* 将间距设置为 30px */
margin-bottom: 0;
}
.demo-form-inline .el-form-item:last-child {
margin-right: 0; /* 移除最后一个元素的右边距 */
}
.form-item {
flex: 1;
}
.form-item :deep(.el-form-item__content) {
width: 100%;
}
.form-item :deep(.el-input),
.form-item :deep(.el-select) {
width: 100%;
}
.search-buttons {
white-space: nowrap;
}
:deep(.operation-buttons .el-button) {
padding: 4px 8px;
margin: 0 2px;
}
:deep(.operation-column) {
background-color: #fff;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
}
.el-button.is-text {
min-width: 32px !important;
}
.dialog-footer {
display: flex;
justify-content: center;
align-items: center;
}
.dialog-footer .el-button {
margin: 0 10px;
}
.delete-content {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.warning-icon {
font-size: 24px;
color: #e6a23c;
margin-right: 10px;
}
/* 可以删除或注释掉之前的 .el-button.is-text 样式,如果不再需要的话 */
/* .el-button.is-text {
min-width: 32px !important;
} */
/* 添加以下样式来使对话框垂直居中 */
:deep(.delete-dialog.el-dialog) {
margin-top: 0 !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -0,0 +1,336 @@
<template>
<div class="project-progress-container">
<!-- 左侧固定列表格 -->
<div class="left-section">
<h2 class="mb20 textC">项目执行表</h2>
<CustomTable
:columns="fixedColumns"
:tableData="executionData"
:showPagination="false"
:maxHeight="tableHeight"
:showSummary="true"
:summaryMethod="getFixedColumnsSummaries"
>
<template #name="{ row }">
<span class="project-name" @click="goToDetail(row)">{{ row.name }}</span>
</template>
</CustomTable>
</div>
<!-- 右侧滚动列表格 -->
<div class="right-section">
<div class="date-range-container">
<span class="date-range-label">统计时间</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateRangeChange"
/>
</div>
<CustomTable
:columns="scrollableColumns"
:tableData="executionData"
:showPagination="false"
:maxHeight="tableHeight"
:showSummary="true"
:summaryMethod="getScrollableColumnsSummaries"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import CustomTable from '@/components/table.vue'
const router = useRouter()
const dateRange = ref(getDefaultDateRange())
const tableHeight = ref('100%')
function getDefaultDateRange() {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
return [startOfMonth, endOfMonth]
}
const fixedColumns = [
{ prop: 'name', label: '项目', width: 200, slot: 'name' },
{ prop: 'presetDays', label: '预计工时\n', width: 150 },
{ prop: 'actualDays', label: '累计工时\n', width: 150 },
]
const scrollableColumns = computed(() => {
if (dateRange.value && dateRange.value.length === 2) {
const start = new Date(dateRange.value[0])
const end = new Date(dateRange.value[1])
const days = []
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dayOfWeek = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][d.getDay()]
const dateStr = `${d.getMonth() + 1}/${d.getDate()}`
days.push({
prop: dateStr,
label: `${dayOfWeek}\n${dateStr}`,
minWidth: 100
})
}
return days
}
return []
})
const executionData = computed(() => {
const projectList = [
{ id: 1, name: '项目1', presetDays: 20, actualDays: 21 },
{ id: 2, name: '项目2', presetDays: 20, actualDays: 13 },
{ id: 3, name: '项目3', presetDays: 20, actualDays: 15 },
{ id: 4, name: '项目4', presetDays: 20, actualDays: 17 },
{ id: 5, name: '项目5', presetDays: 20, actualDays: 18 },
]
return projectList.map(project => {
const data = {
...project,
}
if (dateRange.value && dateRange.value.length === 2) {
const start = new Date(dateRange.value[0])
const end = new Date(dateRange.value[1])
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = `${d.getMonth() + 1}/${d.getDate()}`
data[dateStr] = Math.floor(Math.random() * 8) // 0-8
}
}
return data
})
})
const handleDateRangeChange = () => {
//
}
const getFixedColumnsSummaries = param => {
const { columns, data } = param
const sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计工时(天)'
return
}
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
sums[index] = Number(sums[index].toFixed(2))
} else {
sums[index] = ''
}
})
return sums
}
const getScrollableColumnsSummaries = param => {
const { columns, data } = param
const sums = []
columns.forEach((column, index) => {
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
sums[index] = Number(sums[index].toFixed(2))
} else {
sums[index] = ''
}
})
return sums
}
const updateTableHeight = () => {
nextTick(() => {
const container = document.querySelector('.project-execution-table')
if (container) {
const containerHeight = container.clientHeight
const otherElementsHeight =
container.querySelector('h2').offsetHeight + container.querySelector('.date-range').offsetHeight
tableHeight.value = `${containerHeight - otherElementsHeight - 40}px` // 40px for margins and paddings
}
})
}
const goToDetail = (row) => {
router.push({
name: 'ProjectDetail',
params: { id: row.id },
query: {
startDate: dateRange.value[0].toISOString().split('T')[0],
endDate: dateRange.value[1].toISOString().split('T')[0]
}
})
}
onMounted(() => {
window.addEventListener('resize', updateTableHeight)
updateTableHeight()
})
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight)
})
</script>
<style scoped>
.project-progress-container {
display: flex;
height: 88vh;
background-color: white;
}
.left-section,
.right-section {
display: flex;
flex-direction: column;
height: 100%;
}
.left-section {
width: 400px; /* 根据固定列的总宽度调整 */
padding-top: 34px;
padding-bottom: 20px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); /* 添加右侧阴影 */
z-index: 1; /* 确保左侧在右侧之上 */
margin-right: 5px;
}
.right-section {
flex: 1;
overflow-x: auto;
padding: 20px;
padding-left: 0; /* 移除左侧内边距,与左侧部分紧密相连 */
}
.placeholder-header {
height: 102px; /* 与左侧表格的标题和日期选择器高度一致 */
}
.date-range {
margin-bottom: 20px;
}
:deep(.el-table) {
/* border: 1px solid #dcdfe6; */
height: 100% !important;
}
:deep(.el-table__header th) {
background-color: #4a4a4a;
color: white;
text-align: center;
}
:deep(.el-table__body td) {
text-align: center;
}
:deep(.el-table__footer td) {
background-color: #f5f7fa;
font-weight: bold;
text-align: center; /* 确保合计行内容居中 */
}
:deep(.el-table__header .cell),
:deep(.el-table__body .cell),
:deep(.el-table__footer .cell) {
white-space: pre-wrap;
line-height: 1.2;
padding: 8px 0;
}
:deep(.el-table__body-wrapper) {
overflow-x: hidden;
}
/* 确保两个表格的高度一致 */
.left-section :deep(.el-table),
.right-section :deep(.el-table) {
height: 100% !important;
}
/* 调整日期选择器样式 */
:deep(.el-date-editor) {
width: 100%;
}
/* 调整表格内容的字体大小 */
:deep(.el-table) {
font-size: 14px;
}
/* 调整表头的样式 */
:deep(.el-table__header-wrapper) {
background-color: #f5f7fa;
}
.rightTab :deep(.el-table__header th) {
background-color: #fff;
}
:deep(.el-table__header th) {
background-color: #f5f7fa;
color: #606266;
font-weight: bold;
}
/* 调整合计行的样式 */
:deep(.el-table__footer-wrapper) {
background-color: #f5f7fa;
}
:deep(.el-table__footer td) {
background-color: #f5f7fa !important;
font-weight: bold;
color: #606266;
}
.date-range-container {
display: flex;
align-items: center;
margin-bottom: 20px;
width: 500px;
margin-left: 20px;
}
.date-range-label {
white-space: nowrap;
margin-right: 10px;
}
:deep(.el-date-editor.el-input__wrapper) {
flex: 1;
}
.custom-table {
height: 100%;
}
.project-name {
cursor: pointer;
color: #409EFF;
}
.project-name:hover {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,316 @@
<template>
<div class="project-progress-container">
<!-- 左侧项目信息表单 -->
<div class="left-section">
<h3 class="mb20 ml10 textl">项目人员表</h3>
<el-form :model="projectInfo" label-width="120px" class="project-info-form">
<el-form-item label="选择项目">
<el-select v-model="selectedProject" placeholder="请选择项目" @change="handleProjectChange">
<el-option
v-for="project in projectList"
:key="project.id"
:label="project.name"
:value="project.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="项目名称">
<span>{{ projectInfo.name }}</span>
</el-form-item>
<el-form-item label="项目编码">
<span>{{ projectInfo.code }}</span>
</el-form-item>
<el-form-item label="预计工时">
<span>{{ projectInfo.presetDays }} </span>
</el-form-item>
<el-form-item label="项目开始时间">
<span>{{ projectInfo.startDate }}</span>
</el-form-item>
<el-form-item label="项目结束时间">
<span>{{ projectInfo.endDate }}</span>
</el-form-item>
</el-form>
</div>
<!-- 右侧滚动列表格 -->
<div class="right-section">
<div class="date-range-container">
<span class="date-range-label">统计时间</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateRangeChange"
/>
</div>
<CustomTable
:columns="scrollableColumns"
:tableData="executionData"
:showPagination="false"
:maxHeight="tableHeight"
@button-click="handleButtonClick"
>
</CustomTable>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import CustomTable from '@/components/table.vue'
const router = useRouter()
const dateRange = ref(getDefaultDateRange())
const tableHeight = ref('100%')
const selectedProject = ref(null)
const projectInfo = ref({
name: '',
code: '',
presetDays: '',
startDate: '',
endDate: '',
})
const projectList = [
{ id: 1, name: '项目1', code: 'XM001', presetDays: 120, startDate: '2024-09-01', endDate: '2024-09-30' },
{ id: 2, name: '项目2', code: 'XM002', presetDays: 90, startDate: '2024-10-01', endDate: '2024-12-31' },
// ...
]
function getDefaultDateRange() {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
return [startOfMonth, endOfMonth]
}
const scrollableColumns = computed(() => {
if (dateRange.value && dateRange.value.length === 2) {
const start = new Date(dateRange.value[0])
const end = new Date(dateRange.value[1])
const days = []
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dayOfWeek = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][d.getDay()]
const dateStr = `${d.getMonth() + 1}/${d.getDate()}`
days.push({
prop: dateStr,
label: `${dayOfWeek}\n${dateStr}`,
minWidth: 100,
type: 'multiButton'
})
}
return days
}
return []
})
const executionData = computed(() => {
// 使
const data = {
'张三': ['2023/5/1', '2023/5/2', '2023/5/3'],
'李四': ['2023/5/1', '2023/5/3', '2023/5/4'],
'王五': ['2023/5/2', '2023/5/3', '2023/5/5'],
'赵六': ['2023/5/1', '2023/5/4', '2023/5/5'],
'钱七': ['2023/5/2', '2023/5/4', '2023/5/5']
}
const result = {}
if (dateRange.value && dateRange.value.length === 2) {
const start = new Date(dateRange.value[0])
const end = new Date(dateRange.value[1])
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = `${d.getMonth() + 1}/${d.getDate()}`
result[dateStr] = Object.keys(data).filter(name =>
data[name].includes(`${d.getFullYear()}/${dateStr}`)
)
}
}
return [result] // el-table
})
const handleDateRangeChange = () => {
//
}
const handleProjectChange = projectId => {
const selectedProjectInfo = projectList.find(project => project.id === projectId)
if (selectedProjectInfo) {
projectInfo.value = { ...selectedProjectInfo }
}
}
const handleButtonClick = ({ userName, date }) => {
console.log(`点击了 ${userName}${date} 的工作日志`)
//
router.push({
name: 'WorkLog',
params: { userId: userName },
query: {
date: date,
projectId: selectedProject.value
}
})
}
</script>
<style scoped>
.project-progress-container {
display: flex;
height: 88vh;
background-color: white;
}
.left-section,
.right-section {
display: flex;
flex-direction: column;
height: 100%;
}
.left-section {
width: 400px; /* 根据固定列的总宽度调整 */
padding-top: 34px;
padding-bottom: 20px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); /* 添加右侧阴影 */
z-index: 1; /* 确保左侧在右侧之上 */
margin-right: 5px;
position: relative;
}
.right-section {
flex: 1;
overflow-x: auto;
padding: 20px;
padding-left: 0; /* 移除左侧内边距,与左侧部分紧密相连 */
}
.placeholder-header {
height: 102px; /* 与左侧表格的标题和日期选择器高度一致 */
}
.date-range {
margin-bottom: 20px;
}
:deep(.el-table) {
/* border: 1px solid #dcdfe6; */
height: 100% !important;
}
:deep(.el-table__header th) {
background-color: #4a4a4a;
color: white;
text-align: center;
}
:deep(.el-table__body td) {
text-align: center;
}
:deep(.el-table__footer td) {
background-color: #f5f7fa;
font-weight: bold;
text-align: center; /* 确保合计行内容居中 */
}
:deep(.el-table__header .cell),
:deep(.el-table__body .cell),
:deep(.el-table__footer .cell) {
white-space: pre-wrap;
line-height: 1.2;
padding: 8px 0;
}
:deep(.el-table__body-wrapper) {
overflow-x: hidden;
}
/* 确保两个表格的高度一致 */
.left-section :deep(.el-table),
.right-section :deep(.el-table) {
height: 100% !important;
}
/* 调整日期选择器样式 */
:deep(.el-date-editor) {
width: 100%;
}
/* 调整表内容的字体大小 */
:deep(.el-table) {
font-size: 14px;
}
/* 调整表头的样式 */
:deep(.el-table__header-wrapper) {
background-color: #f5f7fa;
}
.rightTab :deep(.el-table__header th) {
background-color: #fff;
}
:deep(.el-table__header th) {
background-color: #f5f7fa;
color: #606266;
font-weight: bold;
}
/* 调整合计行的样式 */
:deep(.el-table__footer-wrapper) {
background-color: #f5f7fa;
}
:deep(.el-table__footer td) {
background-color: #f5f7fa !important;
font-weight: bold;
color: #606266;
}
.date-range-container {
display: flex;
align-items: center;
margin-bottom: 20px;
width: 500px;
margin-left: 20px;
}
.date-range-label {
white-space: nowrap;
margin-right: 10px;
}
:deep(.el-date-editor.el-input__wrapper) {
flex: 1;
}
.custom-table {
height: 100%;
}
.project-name {
cursor: pointer;
color: #409eff;
}
.project-name:hover {
text-decoration: underline;
}
.user-select-container {
position: absolute;
top: 25px;
right: 10px;
width: 220px;
z-index: 2;
display: flex;
align-items: center;
flex-direction: row;
gap: 10px;
span {
width: 100px;
}
}
</style>

View File

@ -0,0 +1,380 @@
<template>
<div class="project-progress-container">
<!-- 左侧固定列表格 -->
<div class="left-section">
<h3 class="mb20 ml10 textl">人员项目表</h3>
<div class="user-select-container">
<span>选择人员</span>
<el-input v-model="selectedUserName" placeholder="请选择用户" readonly @click="openUserSelectDialog"></el-input>
</div>
<CustomTable
:columns="fixedColumns"
:tableData="executionData"
:showPagination="false"
:maxHeight="tableHeight"
:showSummary="true"
:summaryMethod="getFixedColumnsSummaries"
:border="true"
>
<template #name="{ row }">
<span class="project-name" @click="goToDetail(row)">{{ row.name }}</span>
</template>
</CustomTable>
</div>
<!-- 右侧滚动列表格 -->
<div class="right-section">
<div class="date-range-container">
<span class="date-range-label">统计时间</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateRangeChange"
/>
</div>
<CustomTable
:columns="scrollableColumns"
:tableData="executionData"
:showPagination="false"
:maxHeight="tableHeight"
:showSummary="true"
:summaryMethod="getScrollableColumnsSummaries"
/>
</div>
<!-- 用户选择对话框 -->
<SelectUser
v-model:dialogVisible="userSelectDialogVisible"
:multiSelect="false"
:currentSelectedUser="selectedUser ? [selectedUser] : []"
@confirm="handleUserSelect"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import CustomTable from '@/components/table.vue'
import SelectUser from '@/components/selectUser.vue'
const router = useRouter()
const dateRange = ref(getDefaultDateRange())
const tableHeight = ref('100%')
const userSelectDialogVisible = ref(false)
const selectedUser = ref(null)
const selectedUserName = computed(() => (selectedUser.value ? selectedUser.value.name : ''))
function getDefaultDateRange() {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
return [startOfMonth, endOfMonth]
}
const fixedColumns = [
{ prop: 'name', label: '项目', slot: 'name' },
{ prop: 'presetDays', label: '统计工时\n' },
]
const scrollableColumns = computed(() => {
if (dateRange.value && dateRange.value.length === 2) {
const start = new Date(dateRange.value[0])
const end = new Date(dateRange.value[1])
const days = []
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dayOfWeek = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][d.getDay()]
const dateStr = `${d.getMonth() + 1}/${d.getDate()}`
days.push({
prop: dateStr,
label: `${dayOfWeek}\n${dateStr}`,
minWidth: 100,
})
}
return days
}
return []
})
const executionData = computed(() => {
const projectList = [
{ id: 1, name: '项目1', presetDays: 20 },
{ id: 2, name: '项目2', presetDays: 20 },
{ id: 3, name: '项目3', presetDays: 20 },
{ id: 4, name: '项目4', presetDays: 20 },
{ id: 5, name: '项目5', presetDays: 20 },
]
return projectList.map(project => {
const data = {
...project,
}
if (dateRange.value && dateRange.value.length === 2) {
const start = new Date(dateRange.value[0])
const end = new Date(dateRange.value[1])
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dateStr = `${d.getMonth() + 1}/${d.getDate()}`
data[dateStr] = Math.floor(Math.random() * 8) // 0-8
}
}
return data
})
})
const handleDateRangeChange = () => {
//
}
const getFixedColumnsSummaries = param => {
const { columns, data } = param
const sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计工时(天)'
return
}
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
sums[index] = Number(sums[index].toFixed(2))
} else {
sums[index] = ''
}
})
return sums
}
const getScrollableColumnsSummaries = param => {
const { columns, data } = param
const sums = []
columns.forEach((column, index) => {
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
sums[index] = Number(sums[index].toFixed(2))
} else {
sums[index] = ''
}
})
return sums
}
const updateTableHeight = () => {
nextTick(() => {
const container = document.querySelector('.project-execution-table')
if (container) {
const containerHeight = container.clientHeight
const otherElementsHeight =
container.querySelector('h2').offsetHeight + container.querySelector('.date-range').offsetHeight
tableHeight.value = `${containerHeight - otherElementsHeight - 40}px` // 40px for margins and paddings
}
})
}
const goToDetail = row => {
router.push({
name: 'ProjectDetail',
params: { id: row.id },
query: {
startDate: dateRange.value[0].toISOString().split('T')[0],
endDate: dateRange.value[1].toISOString().split('T')[0],
},
})
}
const openUserSelectDialog = () => {
userSelectDialogVisible.value = true
}
const handleUserSelect = users => {
if (users.length > 0) {
selectedUser.value = users[0]
} else {
selectedUser.value = null
}
}
onMounted(() => {
window.addEventListener('resize', updateTableHeight)
updateTableHeight()
})
onUnmounted(() => {
window.removeEventListener('resize', updateTableHeight)
})
</script>
<style scoped>
.project-progress-container {
display: flex;
height: 88vh;
background-color: white;
}
.left-section,
.right-section {
display: flex;
flex-direction: column;
height: 100%;
}
.left-section {
width: 400px; /* 根据固定列的总宽度调整 */
padding-top: 34px;
padding-bottom: 20px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); /* 添加右侧阴影 */
z-index: 1; /* 确保左侧在右侧之上 */
margin-right: 5px;
position: relative;
}
.right-section {
flex: 1;
overflow-x: auto;
padding: 20px;
padding-left: 0; /* 移除左侧内边距,与左侧部分紧密相连 */
}
.placeholder-header {
height: 102px; /* 与左侧表格的标题和日期选择器高度一致 */
}
.date-range {
margin-bottom: 20px;
}
:deep(.el-table) {
/* border: 1px solid #dcdfe6; */
height: 100% !important;
}
:deep(.el-table__header th) {
background-color: #4a4a4a;
color: white;
text-align: center;
}
:deep(.el-table__body td) {
text-align: center;
}
:deep(.el-table__footer td) {
background-color: #f5f7fa;
font-weight: bold;
text-align: center; /* 确保合计行内容居中 */
}
:deep(.el-table__header .cell),
:deep(.el-table__body .cell),
:deep(.el-table__footer .cell) {
white-space: pre-wrap;
line-height: 1.2;
padding: 8px 0;
}
:deep(.el-table__body-wrapper) {
overflow-x: hidden;
}
/* 确保两个表格的高度一致 */
.left-section :deep(.el-table),
.right-section :deep(.el-table) {
height: 100% !important;
}
/* 调整日期选择器样式 */
:deep(.el-date-editor) {
width: 100%;
}
/* 调整表<E695B4><E8A1A8><EFBFBD>内容的字体大小 */
:deep(.el-table) {
font-size: 14px;
}
/* 调整表头的样式 */
:deep(.el-table__header-wrapper) {
background-color: #f5f7fa;
}
.rightTab :deep(.el-table__header th) {
background-color: #fff;
}
:deep(.el-table__header th) {
background-color: #f5f7fa;
color: #606266;
font-weight: bold;
}
/* 调整合计行的样式 */
:deep(.el-table__footer-wrapper) {
background-color: #f5f7fa;
}
:deep(.el-table__footer td) {
background-color: #f5f7fa !important;
font-weight: bold;
color: #606266;
}
.date-range-container {
display: flex;
align-items: center;
margin-bottom: 20px;
width: 500px;
margin-left: 20px;
}
.date-range-label {
white-space: nowrap;
margin-right: 10px;
}
:deep(.el-date-editor.el-input__wrapper) {
flex: 1;
}
.custom-table {
height: 100%;
}
.project-name {
cursor: pointer;
color: #409eff;
}
.project-name:hover {
text-decoration: underline;
}
.user-select-container {
position: absolute;
top: 25px;
right: 10px;
width: 220px;
z-index: 2;
display: flex;
align-items: center;
flex-direction: row;
gap:10px;
span{
width: 100px;
}
}
</style>

View File

@ -0,0 +1,576 @@
<template>
<div class="work-log-container">
<!-- 左侧项目列表 -->
<div class="project-list" :class="{ collapsed: isCollapsed }">
<div class="list-header">
<span v-if="!isCollapsed"></span>
<div class="collapse-button-wrapper">
<el-button type="text" @click="toggleCollapse" class="collapse-button">
<el-icon :size="20">
<ArrowLeft v-if="!isCollapsed" />
<ArrowRight v-else />
</el-icon>
</el-button>
</div>
</div>
<el-table
v-show="!isCollapsed"
:data="projectList"
style="width: 100%"
@row-click="handleProjectClick"
highlight-current-row
:show-summary="true"
:summary-method="getSummaries"
sum-text="合计工时h"
>
<el-table-column prop="name" label="项目" align="center"></el-table-column>
<el-table-column prop="workHours" label="工时(小时)" align="center"></el-table-column>
</el-table>
</div>
<!-- 右侧项目信息和日历 -->
<div class="project-info-calendar">
<h2 class="mb20 textC">工作日志</h2>
<!-- 项目信息表单 -->
<el-form :model="projectInfo" label-width="100px" disabled class="project-info-form">
<el-form-item label="项目名称">
<el-input v-model="projectInfo.name"></el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目编号">
<el-input v-model="projectInfo.code"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工时填报人">
<el-input v-model="projectInfo.reporter"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目开始时间">
<el-date-picker v-model="projectInfo.startDate" type="date"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目结束时间">
<el-date-picker v-model="projectInfo.endDate" type="date"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 日历选择器 -->
<!-- 日历视图 -->
<div class="calendar-view" v-if="currentProject">
<el-calendar v-model="selectedDate" @input="handleMonthChange">
<template #header="{ date }">
<div class="calendar-header">
<el-date-picker v-model="selectedDate" type="month" format="YYYY年MM月" :clearable="false" />
</div>
</template>
<template #dateCell="{ data }">
<div
@click="openLogDialog(data)"
:class="{
'date-cell': true,
'in-range': isInProjectRange(data),
'out-range': !isInProjectRange(data),
disabled: isFutureDate(data.date) && isInProjectRange(data),
}"
>
{{ data.day.split('-')[2] }}
</div>
</template>
</el-calendar>
</div>
</div>
<!-- 工作日志对话框 -->
<el-dialog v-model="logDialogVisible" title="工作日志" width="30%">
<el-form :model="logForm" label-width="100px" class="log-form">
<el-form-item label="日期">
<el-input v-model="logForm.date" disabled></el-input>
</el-form-item>
<el-form-item label="工作内容">
<el-input v-model="logForm.content" type="textarea" :rows="4"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="logDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveWorkLog"></el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
const isCollapsed = ref(false)
const selectedDate = ref(new Date())
const currentMonth = ref(new Date())
//
const projectList = ref([
{ id: 1, name: '项目1', startDate: '2023-01-01', endDate: '2023-06-30', workHours: 120 },
{ id: 2, name: '项目2', startDate: '2023-03-15', endDate: '2023-12-31', workHours: 80 },
{ id: 3, name: '项目3', startDate: '2023-02-01', endDate: '2023-08-31', workHours: 160 },
])
//
const currentProject = ref(null)
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
const handleProjectClick = row => {
currentProject.value = row
//
}
const openLogDialog = data => {
if (!currentProject.value) {
ElMessage.warning('请先选择一个项目')
return
}
// 使 data.day data.date
const clickedDate = new Date(data.day)
const currentDate = new Date()
// UTC
clickedDate.setUTCHours(0, 0, 0, 0)
currentDate.setUTCHours(0, 0, 0, 0)
if (clickedDate.getTime() > currentDate.getTime()) {
ElMessage.warning('不可编辑未来日期')
return
}
const date = new Date(data.day)
const start = new Date(currentProject.value.startDate)
const end = new Date(currentProject.value.endDate)
// 使 getTime()
let flag = date.getTime() >= start.getTime() && date.getTime() <= end.getTime()
console.log(currentProject.value.endDate, currentProject.value.startDate, data.day)
if (!flag) {
ElMessage.warning('该日期不在项目范围内')
return
}
//
logDialogVisible.value = true
}
const isInProjectRange = data => {
if (!currentProject.value) return fals
const date = new Date(data.day)
const start = new Date(currentProject.value.startDate)
const end = new Date(currentProject.value.endDate)
// 使 getTime()
return date.getTime() >= start.getTime() && date.getTime() <= end.getTime()
}
const isFutureDate = date => {
const clickedDate = new Date(date)
const currentDate = new Date()
return clickedDate.getTime() > currentDate.getTime()
}
const getSummaries = param => {
const { columns, data } = param
const sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计工时h'
return
}
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr)
if (!isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)
} else {
sums[index] = 'N/A'
}
})
return sums
}
const logForm = ref({
date: '',
content: '',
})
//
const projectInfo = ref({
name: '项目1',
code: 'XM5836383',
reporter: '张三',
startDate: '2024-6-15',
endDate: '2024-8-29',
})
//
const logDialogVisible = ref(false)
const workLog = ref({
hours: 0,
content: '',
})
//
const saveWorkLog = () => {
//
console.log('保存工作日志', workLog.value)
logDialogVisible.value = false
}
const handleMonthChange = date => {
currentMonth.value = date
}
const selectDate = type => {
const date = new Date(currentMonth.value)
switch (type) {
case 'prev-month':
date.setMonth(date.getMonth() - 1)
break
case 'today':
date = new Date()
break
case 'next-month':
date.setMonth(date.getMonth() + 1)
break
}
currentMonth.value = date
selectedDate.value = date
}
//
watch(currentProject, newProject => {
if (newProject) {
selectedDate.value = new Date(newProject.startDate)
currentMonth.value = new Date(newProject.startDate)
}
})
const route = useRoute()
onMounted(() => {
const userId = route.params.userId
const userName = route.query.userName
if (userId && userName) {
console.log(`加载用户 ${userName}ID: ${userId})的工作日志`)
//
}
// ... ...
})
</script>
<style scoped>
.work-log-container {
display: flex;
height: 88vh;
background-color: white;
}
.project-list {
width: 300px;
border-right: 1px solid #dcdfe6;
transition: all 0.3s;
overflow: hidden;
position: relative;
}
.project-list.collapsed {
width: 50px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
font-size: 16px;
font-weight: bold;
border-bottom: 1px solid #dcdfe6;
position: relative;
height: 40px;
}
.collapse-button-wrapper {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 40px; /* 固定宽度 */
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.project-list.collapsed .collapse-button-wrapper {
width: 100%;
}
.collapse-button {
padding: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.collapse-button:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.collapse-button :deep(.el-icon) {
font-size: 20px;
color: #606266;
}
.project-list :deep(.el-table__body-wrapper) {
overflow-y: auto;
}
.project-list :deep(.el-table__footer-wrapper) {
position: sticky;
bottom: 0;
z-index: 1;
}
.project-list :deep(.el-table__footer) {
background-color: white;
}
.project-list :deep(.el-table__footer td) {
background-color: white !important;
font-weight: bold;
}
.project-list :deep(.el-table__body td),
.project-list :deep(.el-table__footer td) {
height: 60px; /* 增加单格度 */
}
.project-info-calendar {
flex: 1;
padding: 20px;
overflow-y: auto;
padding-bottom: 0;
}
.calendar-picker {
margin-bottom: 0;
}
.calendar-view :deep(.el-calendar-day) {
padding: 4px; /* 恢复默认内边距 */
}
.calendar-view :deep(.el-calendar-day:hover) {
cursor: pointer;
background-color: #f2f6fc;
}
.calendar-view :deep(.date-cell) {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.calendar-view :deep(.in-range) {
background-color: #ecf5ff;
}
.calendar-view :deep(.out-range) {
background-color: rgba(0, 0, 0, 0.05);
}
.calendar-view :deep(.disabled) {
background-color: #fff;
color: #c0c4cc;
cursor: not-allowed;
}
.dialog-footer {
display: flex;
justify-content: center;
margin-top: 20px;
}
.project-list :deep(.el-table .cell) {
text-align: center;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.calendar-view :deep(.el-form-item) {
margin-bottom: 20px;
}
.calendar-view :deep(.el-input__inner) {
height: 40px;
}
.calendar-view :deep(.el-textarea__inner) {
min-height: 100px;
}
.calendar-view :deep(.el-calendar-header) {
display: flex;
justify-content: center;
padding: 0;
}
.log-form :deep(.el-form-item) {
margin-bottom: 30px;
}
.log-form :deep(.el-input__wrapper) {
height: 50px;
}
.log-form :deep(.el-input__inner) {
height: 50px;
line-height: 50px;
font-size: 16px;
}
.log-form :deep(.el-textarea__inner) {
min-height: 150px;
font-size: 16px;
line-height: 1.5;
padding: 12px 15px;
}
.log-form :deep(.el-input__wrapper) {
height: 50px;
}
.log-form :deep(.el-input__inner) {
height: 50px;
line-height: 50px;
font-size: 16px;
}
.log-form :deep(.el-textarea__inner) {
min-height: 150px;
font-size: 16px;
line-height: 1.5;
padding: 12px 15px;
}
/* 增加 el-form-item__label 的高度和行高 */
:deep(.el-form-item__label) {
height: 42px; /* 默认通常是 32px所以增加 10px 后变为 42px */
line-height: 42px; /* 行高与高度相同,确保文字垂直居中 */
}
/* 保持 el-form-item__content 的一致性 */
:deep(.el-form-item__content) {
min-height: 42px;
display: flex;
align-items: center;
}
/* 调整输入框的高度以匹配增加的空间 */
:deep(.el-input__wrapper) {
height: 42px;
}
:deep(.el-input__inner) {
height: 42px;
line-height: 42px;
}
/* 对于 textarea可能需要单独调整 */
:deep(.el-textarea__inner) {
min-height: 42px;
}
:deep(.el-calendar__header) {
justify-content: center !important;
}
:deep(.el-calendar__body) {
padding-bottom: 0;
}
.calendar-view {
flex: 1;
padding: 20px;
font-size: 14px; /* 缩小字体大小 */
}
.calendar-view :deep(.el-calendar) {
--el-calendar-cell-width: 40px; /* 缩小日历单元格宽度 */
}
.calendar-view :deep(.el-calendar__header) {
padding: 10px 0; /* 减小头部内边距 */
}
.calendar-view :deep(.el-calendar__body) {
padding: 10px 0; /* 减小主体内边距 */
}
.calendar-view :deep(.el-calendar__week) {
background-color: #4a4a4a; /* 深灰色背景 */
color: white; /* 白色文字 */
padding: 5px 0; /* 增加一些内边距 */
}
.calendar-view :deep(.el-calendar-day) {
height: 65px; /* 增加日期单元格高度(原高度 + 5px */
padding-top: 5px;
padding-bottom: 5px;
}
.project-list :deep(.el-table) {
width: 100% !important;
}
.project-list :deep(.el-table__body-wrapper) {
overflow-x: hidden;
}
.project-list :deep(.el-table .cell) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.el-dialog__body) {
padding-bottom: 0 !important;
}
.date-cell {
cursor: pointer;
}
.date-cell.disabled {
cursor: not-allowed;
}
</style>