增加基础页面
This commit is contained in:
parent
705ffbe78c
commit
162f52a8c5
|
|
@ -146,6 +146,70 @@
|
|||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/attendance-punch/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "自有考勤打卡",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/attendance-punch/location/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "位置选择打卡",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/attendance-statistics/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "自有考勤统计",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/electronic-contract/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "自有电子合同",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/message-notification/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "自有消息通知",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/message-notification/detail/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "公告",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/payslip/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "自有工资条",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/own/payslip/detail/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工资条",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarBackgroundColor": "#07c160"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
|
|
|||
|
|
@ -10,24 +10,124 @@
|
|||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content" :style="contentStyle">
|
||||
<view class="placeholder">
|
||||
<text class="placeholder-text">考勤统计页面</text>
|
||||
<text class="placeholder-desc">此页面待完善,请稍后...</text>
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 提示信息 -->
|
||||
<view class="alert-section" @tap="handleAlertClick">
|
||||
<text class="alert-text"
|
||||
>还有{{ pendingReviewCount }}人考勤未审核,点击查看详情</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 工程信息 -->
|
||||
<view class="project-info">
|
||||
<text class="project-name">{{ projectName }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏(固定) -->
|
||||
<view class="search-section">
|
||||
<!-- 人员姓名搜索 -->
|
||||
<view class="search-row">
|
||||
<view style="flex: 1">
|
||||
<up-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="人员姓名"
|
||||
:clearable="true"
|
||||
@input="handleSearchInput"
|
||||
@confirm="handleSearch"
|
||||
customStyle="background: #f5f5f5; border-radius: 8rpx; padding: 0 24rpx; height: 64rpx;"
|
||||
/>
|
||||
</view>
|
||||
<view>
|
||||
<up-button
|
||||
text="搜索"
|
||||
type="primary"
|
||||
size="small"
|
||||
:customStyle="buttonStyle"
|
||||
@tap="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期范围选择 -->
|
||||
<view class="date-range-row">
|
||||
<view style="flex: 1">
|
||||
<DatePicker
|
||||
v-model="startDate"
|
||||
format="YYYY-MM-DD"
|
||||
placeholder="选择开始日期"
|
||||
iconColor="#ff6b35"
|
||||
@change="handleStartDateChange"
|
||||
/>
|
||||
</view>
|
||||
<view style="flex: 1">
|
||||
<DatePicker
|
||||
v-model="endDate"
|
||||
format="YYYY-MM-DD"
|
||||
placeholder="选择结束日期"
|
||||
iconColor="#ff6b35"
|
||||
@change="handleEndDateChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表格内容(可滚动) -->
|
||||
<scroll-view class="table-container" scroll-y>
|
||||
<view class="table-wrapper">
|
||||
<!-- 表头 -->
|
||||
<view class="table-header">
|
||||
<view class="table-cell" style="width: 80rpx">序号</view>
|
||||
<view class="table-cell" style="flex: 1">姓名</view>
|
||||
<view class="table-cell" style="width: 120rpx">考勤</view>
|
||||
<view class="table-cell" style="width: 120rpx">上班</view>
|
||||
<view class="table-cell" style="width: 120rpx">休息</view>
|
||||
<view class="table-cell" style="width: 120rpx">缺勤</view>
|
||||
</view>
|
||||
|
||||
<!-- 表格数据 -->
|
||||
<view v-if="statisticsList.length === 0" class="empty-state">
|
||||
<ReviewEmptyState text="暂无数据" />
|
||||
</view>
|
||||
<view v-else class="table-body">
|
||||
<view
|
||||
v-for="(item, index) in statisticsList"
|
||||
:key="index"
|
||||
class="table-row"
|
||||
>
|
||||
<view class="table-cell" style="width: 80rpx">{{ index + 1 }}</view>
|
||||
<view class="table-cell" style="flex: 1">{{ item.name }}</view>
|
||||
<view class="table-cell" style="width: 120rpx">{{
|
||||
item.attendance
|
||||
}}</view>
|
||||
<view class="table-cell" style="width: 120rpx">{{ item.work }}</view>
|
||||
<view class="table-cell" style="width: 120rpx">{{ item.rest }}</view>
|
||||
<view class="table-cell" style="width: 120rpx">{{ item.absence }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import DatePicker from '@/components/DatePicker/index.vue'
|
||||
import ReviewEmptyState from '@/components/ReviewEmptyState/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* 考勤统计页面
|
||||
* 业务背景:用于查看考勤统计数据
|
||||
* 设计决策:当前为占位页面,后续将实现具体的统计功能
|
||||
* 业务背景:用于查看考勤统计数据,包括考勤、上班、休息、缺勤等统计信息
|
||||
* 设计决策:
|
||||
* 1. 显示未审核人员数量提示,可点击查看详情
|
||||
* 2. 显示当前工程信息
|
||||
* 3. 支持按人员姓名搜索
|
||||
* 4. 支持按日期范围查询统计
|
||||
* 5. 使用表格形式展示统计数据
|
||||
* 6. 使用scroll-view实现表格滚动
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
|
|
@ -38,6 +138,128 @@ const contentStyle = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
// 未审核人数
|
||||
const pendingReviewCount = ref(13)
|
||||
|
||||
// 工程名称
|
||||
const projectName = ref('丛塘-生药1回入江背220工程(XL)')
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const startDate = ref(dayjs().subtract(1, 'month').valueOf()) // 默认一个月前
|
||||
const endDate = ref(Date.now()) // 默认今天
|
||||
|
||||
// 统计数据列表
|
||||
const statisticsList = ref([
|
||||
{ name: '王万平', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德先', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '安兴江', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德锡', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '雷远忠', attendance: 29.0, work: 29.0, rest: 1.0, absence: 1.0 },
|
||||
{ name: '石景绍', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '黄必华', attendance: 18.0, work: 18.0, rest: 12.0, absence: 1.0 },
|
||||
{ name: '石德明', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德亮', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德强', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德勇', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德刚', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德文', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德武', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德全', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
{ name: '石德胜', attendance: 30.0, work: 30.0, rest: 0.0, absence: 1.0 },
|
||||
])
|
||||
|
||||
// 按钮样式
|
||||
const buttonStyle = computed(() => {
|
||||
return {
|
||||
backgroundColor: '#07c160',
|
||||
borderColor: '#07c160',
|
||||
marginLeft: '16rpx',
|
||||
height: '64rpx',
|
||||
fontSize: '26rpx',
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理搜索输入
|
||||
* @param {String} value - 输入值
|
||||
*/
|
||||
const handleSearchInput = (value) => {
|
||||
searchKeyword.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理开始日期变化
|
||||
* @param {Number} timestamp - 时间戳
|
||||
*/
|
||||
const handleStartDateChange = (timestamp) => {
|
||||
startDate.value = timestamp
|
||||
// 验证开始日期不能晚于结束日期
|
||||
if (endDate.value && timestamp > endDate.value) {
|
||||
uni.showToast({
|
||||
title: '开始日期不能晚于结束日期',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
// TODO: 根据日期范围重新加载数据
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理结束日期变化
|
||||
* @param {Number} timestamp - 时间戳
|
||||
*/
|
||||
const handleEndDateChange = (timestamp) => {
|
||||
endDate.value = timestamp
|
||||
// 验证结束日期不能早于开始日期
|
||||
if (startDate.value && timestamp < startDate.value) {
|
||||
uni.showToast({
|
||||
title: '结束日期不能早于开始日期',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
// TODO: 根据日期范围重新加载数据
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索
|
||||
*/
|
||||
const handleSearch = () => {
|
||||
// TODO: 调用搜索接口
|
||||
console.log('搜索参数:', {
|
||||
keyword: searchKeyword.value,
|
||||
startDate: dayjs(startDate.value).format('YYYY-MM-DD'),
|
||||
endDate: dayjs(endDate.value).format('YYYY-MM-DD'),
|
||||
})
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理提示信息点击
|
||||
*/
|
||||
const handleAlertClick = () => {
|
||||
// TODO: 跳转到未审核列表页面
|
||||
uni.showToast({
|
||||
title: '跳转到未审核列表',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载统计数据
|
||||
*/
|
||||
const loadStatistics = () => {
|
||||
// TODO: 调用接口加载统计数据
|
||||
// 根据搜索关键字和日期范围过滤数据
|
||||
// const filteredList = filterStatisticsList()
|
||||
// statisticsList.value = filteredList
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
|
@ -45,33 +267,117 @@ const handleBack = () => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.alert-section {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
font-size: 26rpx;
|
||||
color: #e34d59;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.date-range-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
background: #f5f7fa;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 24rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
.table-header .table-cell {
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
.empty-state {
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
|
|
|
|||
|
|
@ -10,24 +10,94 @@
|
|||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content" :style="contentStyle">
|
||||
<view class="placeholder">
|
||||
<text class="placeholder-text">上班与休息页面</text>
|
||||
<text class="placeholder-desc">此页面待完善,请稍后...</text>
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 搜索栏(固定) -->
|
||||
<view class="search-section">
|
||||
<!-- 左侧:下拉菜单和输入框 -->
|
||||
<view class="search-left">
|
||||
<CommonPicker
|
||||
v-model="filterStatus"
|
||||
:options="statusOptions"
|
||||
placeholder="全部"
|
||||
@change="handleStatusChange"
|
||||
/>
|
||||
<up-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入关键字"
|
||||
border="none"
|
||||
:clearable="true"
|
||||
@input="handleSearchInput"
|
||||
@confirm="handleSearch"
|
||||
customStyle="background: #f5f5f5; border-radius: 8rpx; padding: 0 24rpx; height: 64rpx; margin-top: 16rpx;"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 右侧:日期选择器和搜索按钮 -->
|
||||
<view class="search-right">
|
||||
<DatePicker
|
||||
v-model="selectedDate"
|
||||
format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
<up-button
|
||||
text="搜索"
|
||||
type="primary"
|
||||
size="small"
|
||||
:customStyle="buttonStyle"
|
||||
@tap="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容(可滚动) -->
|
||||
<scroll-view class="list-container" scroll-y>
|
||||
<ReviewEmptyState v-if="personnelList.length === 0" text="暂无数据" />
|
||||
<view v-else class="personnel-list">
|
||||
<view
|
||||
v-for="(item, index) in personnelList"
|
||||
:key="index"
|
||||
class="personnel-item"
|
||||
>
|
||||
<view class="personnel-header">
|
||||
<text class="personnel-name">{{ item.name }}</text>
|
||||
<text class="personnel-status" :class="getStatusClass(item.status)">
|
||||
{{ item.status }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="personnel-info">
|
||||
<text class="info-label">身份证号:</text>
|
||||
<text class="info-value">{{ item.idNumber }}</text>
|
||||
</view>
|
||||
<view v-if="item.punchTime" class="personnel-info">
|
||||
<text class="info-label">打卡时间:</text>
|
||||
<text class="info-value">{{ item.punchTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import CommonPicker from '@/components/CommonPicker/index.vue'
|
||||
import DatePicker from '@/components/DatePicker/index.vue'
|
||||
import ReviewEmptyState from '@/components/ReviewEmptyState/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* 上班与休息页面
|
||||
* 业务背景:用于管理上班和休息时间设置
|
||||
* 设计决策:当前为占位页面,后续将实现具体的上班与休息功能
|
||||
* 业务背景:用于查看和管理施工人员的打卡记录
|
||||
* 设计决策:
|
||||
* 1. 支持按状态筛选(全部、已打卡、未打卡)
|
||||
* 2. 支持关键字搜索(姓名、身份证号等)
|
||||
* 3. 支持按日期查询打卡记录
|
||||
* 4. 显示打卡状态和打卡时间
|
||||
* 5. 使用scroll-view实现列表滚动
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
|
|
@ -38,6 +108,126 @@ const contentStyle = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = ['全部', '已打卡', '未打卡']
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const filterStatus = ref('全部')
|
||||
const selectedDate = ref(Date.now()) // 默认今天
|
||||
|
||||
// 人员列表数据
|
||||
const personnelList = ref([
|
||||
{
|
||||
name: '丁兴旺',
|
||||
status: '已打卡',
|
||||
idNumber: '36042419920927085X',
|
||||
punchTime: '2025-12-01 06:57:36',
|
||||
},
|
||||
{
|
||||
name: '袁亥良',
|
||||
status: '未打卡',
|
||||
idNumber: '421281199309103315',
|
||||
punchTime: '',
|
||||
},
|
||||
{
|
||||
name: '游木生',
|
||||
status: '已打卡',
|
||||
idNumber: '422323196810263338',
|
||||
punchTime: '2025-12-01 07:13:14',
|
||||
},
|
||||
{
|
||||
name: '熊拥华',
|
||||
status: '已打卡',
|
||||
idNumber: '42232319771116331X',
|
||||
punchTime: '2025-12-01 07:00:45',
|
||||
},
|
||||
{
|
||||
name: '吴庸奎',
|
||||
status: '已打卡',
|
||||
idNumber: '42232319801212331X',
|
||||
punchTime: '2025-12-01 07:05:23',
|
||||
},
|
||||
])
|
||||
|
||||
// 按钮样式
|
||||
const buttonStyle = computed(() => {
|
||||
return {
|
||||
backgroundColor: '#07c160',
|
||||
borderColor: '#07c160',
|
||||
marginTop: '16rpx',
|
||||
minWidth: '120rpx',
|
||||
height: '64rpx',
|
||||
fontSize: '26rpx',
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理搜索输入
|
||||
* @param {String} value - 输入值
|
||||
*/
|
||||
const handleSearchInput = (value) => {
|
||||
searchKeyword.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态变化
|
||||
* @param {String} status - 状态值
|
||||
*/
|
||||
const handleStatusChange = (status) => {
|
||||
filterStatus.value = status
|
||||
// TODO: 根据状态筛选数据
|
||||
loadList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理日期变化
|
||||
* @param {Number} timestamp - 时间戳
|
||||
*/
|
||||
const handleDateChange = (timestamp) => {
|
||||
selectedDate.value = timestamp
|
||||
// TODO: 根据日期重新加载数据
|
||||
loadList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索
|
||||
*/
|
||||
const handleSearch = () => {
|
||||
// TODO: 调用搜索接口
|
||||
console.log('搜索参数:', {
|
||||
keyword: searchKeyword.value,
|
||||
status: filterStatus.value,
|
||||
date: dayjs(selectedDate.value).format('YYYY-MM-DD'),
|
||||
})
|
||||
loadList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态样式类
|
||||
* @param {String} status - 状态值
|
||||
* @returns {String} 样式类名
|
||||
*/
|
||||
const getStatusClass = (status) => {
|
||||
const statusMap = {
|
||||
已打卡: 'status-punched',
|
||||
未打卡: 'status-not-punched',
|
||||
}
|
||||
return statusMap[status] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载列表数据
|
||||
*/
|
||||
const loadList = () => {
|
||||
// TODO: 调用接口加载数据
|
||||
// 根据筛选条件过滤数据
|
||||
// const filteredList = filterPersonnelList()
|
||||
// personnelList.value = filteredList
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
|
@ -45,33 +235,109 @@ const handleBack = () => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
text-align: center;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.personnel-list {
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.personnel-item {
|
||||
background: #fff;
|
||||
border: 1rpx solid #e5e5e5;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.personnel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.personnel-name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
.personnel-status {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.status-punched {
|
||||
color: #07c160;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
.status-not-punched {
|
||||
color: #e34d59;
|
||||
background: rgba(227, 77, 89, 0.1);
|
||||
}
|
||||
|
||||
.personnel-info {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-right: 16rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
|
|
|
|||
|
|
@ -15,74 +15,29 @@
|
|||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content" :style="contentStyle">
|
||||
<!-- 功能模块网格 -->
|
||||
<view class="module-grid">
|
||||
<!-- 第一行 -->
|
||||
<view class="module-item" @tap="handleModuleClick('entry-review')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 功能模块网格(可滚动) -->
|
||||
<scroll-view class="scroll-container" scroll-y>
|
||||
<view class="module-grid">
|
||||
<view
|
||||
v-for="(module, index) in moduleList"
|
||||
:key="index"
|
||||
class="module-item"
|
||||
@tap="handleModuleClick(module.type)"
|
||||
>
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">{{ module.label }}</text>
|
||||
</view>
|
||||
<text class="module-text">施工入场审核</text>
|
||||
</view>
|
||||
|
||||
<view class="module-item" @tap="handleModuleClick('contract-review')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">电子合同审核</text>
|
||||
</view>
|
||||
|
||||
<view class="module-item" @tap="handleModuleClick('contract-sign')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">电子合同签署</text>
|
||||
</view>
|
||||
|
||||
<!-- 第二行 -->
|
||||
<view class="module-item" @tap="handleModuleClick('wage-view')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">工资查看</text>
|
||||
</view>
|
||||
|
||||
<view class="module-item" @tap="handleModuleClick('entry-management')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">入场管理</text>
|
||||
</view>
|
||||
|
||||
<view class="module-item" @tap="handleModuleClick('contract')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">电子合同</text>
|
||||
</view>
|
||||
|
||||
<!-- 第三行 -->
|
||||
<view class="module-item" @tap="handleModuleClick('wage-card')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">工资卡见证</text>
|
||||
</view>
|
||||
|
||||
<view class="module-item" @tap="handleModuleClick('wage-complaint')">
|
||||
<view class="module-icon">
|
||||
<up-icon name="file-text" size="48" color="#07c160" />
|
||||
</view>
|
||||
<text class="module-text">欠薪维权申诉</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
|
|
@ -95,6 +50,7 @@ import { getContentStyle } from '@/utils/safeArea'
|
|||
* 2. 使用国网绿主题色,保持视觉一致性
|
||||
* 3. 适配安全区,确保在嵌入APP时正常显示
|
||||
* 4. 导航栏左侧为工程选择,右侧为扫描功能,符合业务需求
|
||||
* 5. 使用v-for渲染模块列表,减少代码冗余
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -110,22 +66,48 @@ const contentStyle = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
// 模块列表配置
|
||||
const moduleList = ref([
|
||||
// 原有模块
|
||||
{ type: 'entry-review', label: '施工入场审核' },
|
||||
{ type: 'contract-review', label: '电子合同审核' },
|
||||
{ type: 'contract-sign', label: '电子合同签署' },
|
||||
{ type: 'wage-view', label: '工资查看' },
|
||||
{ type: 'entry-management', label: '入场管理' },
|
||||
{ type: 'contract', label: '电子合同' },
|
||||
{ type: 'wage-card', label: '工资卡见证' },
|
||||
{ type: 'wage-complaint', label: '欠薪维权申诉' },
|
||||
// 新增模块
|
||||
{ type: 'own-attendance-punch', label: '自有考勤打卡' },
|
||||
{ type: 'own-attendance-statistics', label: '自有考勤统计' },
|
||||
{ type: 'own-electronic-contract', label: '自有电子合同' },
|
||||
{ type: 'own-message-notification', label: '自有消息通知' },
|
||||
{ type: 'own-payslip', label: '自有工资条' },
|
||||
])
|
||||
|
||||
// 路由映射
|
||||
const routeMap = {
|
||||
'entry-review': '/pages/work/entry-review/index',
|
||||
'contract-review': '/pages/work/contract-review/index',
|
||||
'contract-sign': '/pages/work/contract-sign/index',
|
||||
'wage-view': '/pages/work/wage-view/index',
|
||||
'entry-management': '/pages/work/entry-management/index',
|
||||
contract: '/pages/work/contract/index',
|
||||
'wage-card': '/pages/work/wage-card/index',
|
||||
'wage-complaint': '/pages/work/wage-complaint/index',
|
||||
// 新增模块路由(待创建页面)
|
||||
'own-attendance-punch': '/pages/own/attendance-punch/index',
|
||||
'own-attendance-statistics': '/pages/own/attendance-statistics/index',
|
||||
'own-electronic-contract': '/pages/own/electronic-contract/index',
|
||||
'own-message-notification': '/pages/own/message-notification/index',
|
||||
'own-payslip': '/pages/own/payslip/index',
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理功能模块点击
|
||||
* @param {String} type - 模块类型
|
||||
*/
|
||||
const handleModuleClick = (type) => {
|
||||
const routeMap = {
|
||||
'entry-review': '/pages/work/entry-review/index',
|
||||
'contract-review': '/pages/work/contract-review/index',
|
||||
'contract-sign': '/pages/work/contract-sign/index',
|
||||
'wage-view': '/pages/work/wage-view/index',
|
||||
'entry-management': '/pages/work/entry-management/index',
|
||||
contract: '/pages/work/contract/index',
|
||||
'wage-card': '/pages/work/wage-card/index',
|
||||
'wage-complaint': '/pages/work/wage-complaint/index',
|
||||
}
|
||||
|
||||
const url = routeMap[type]
|
||||
if (url) {
|
||||
uni.navigateTo({
|
||||
|
|
@ -170,20 +152,32 @@ onLoad(() => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.home-container {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 40rpx 32rpx;
|
||||
min-height: calc(100vh - 200rpx);
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
padding-top: 24rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="考勤打卡">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 日期和星期 -->
|
||||
<view class="date-info">
|
||||
<text class="date-text">{{ currentDate }}</text>
|
||||
<text class="weekday-text">{{ currentWeekday }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 打卡状态区域 -->
|
||||
<view class="status-area">
|
||||
<text class="status-text" :class="{ 'status-punched': isPunched }">
|
||||
{{ isPunched ? '今日已打卡' : '今日未打卡' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 地址信息 -->
|
||||
<view class="address-area">
|
||||
<text class="address-label">地址:</text>
|
||||
<text class="address-value">{{ address || '无' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 打卡按钮 -->
|
||||
<view class="punch-button-wrapper">
|
||||
<view class="punch-button" @tap="handlePunchClick">
|
||||
<text class="time-text">{{ currentTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* 自有考勤打卡页面
|
||||
* 业务背景:用于自有人员的考勤打卡功能
|
||||
* 设计决策:
|
||||
* 1. 显示当前日期和星期
|
||||
* 2. 显示打卡状态(已打卡/未打卡)
|
||||
* 3. 显示地址信息
|
||||
* 4. 底部大圆形按钮显示当前时间,点击后跳转到位置选择打卡页面
|
||||
* 5. 实时更新时间显示
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: true,
|
||||
})
|
||||
})
|
||||
|
||||
// 当前时间
|
||||
const currentTime = ref('')
|
||||
// 打卡状态
|
||||
const isPunched = ref(false)
|
||||
// 地址信息
|
||||
const address = ref('')
|
||||
// 定时器
|
||||
let timeInterval = null
|
||||
|
||||
// 当前日期
|
||||
const currentDate = computed(() => {
|
||||
return dayjs().format('YYYY年MM月DD日')
|
||||
})
|
||||
|
||||
// 当前星期
|
||||
const currentWeekday = computed(() => {
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
return weekdays[dayjs().day()]
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新时间显示
|
||||
*/
|
||||
const updateTime = () => {
|
||||
currentTime.value = dayjs().format('HH:mm:ss')
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理打卡按钮点击
|
||||
*/
|
||||
const handlePunchClick = () => {
|
||||
// 跳转到位置选择打卡页面
|
||||
uni.navigateTo({
|
||||
url: '/pages/own/attendance-punch/location/index',
|
||||
fail: (err) => {
|
||||
console.error('导航失败:', err)
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载打卡状态和地址信息
|
||||
*/
|
||||
const loadPunchInfo = () => {
|
||||
// TODO: 调用接口获取打卡状态和地址信息
|
||||
// const res = await getPunchInfo()
|
||||
// isPunched.value = res.isPunched
|
||||
// address.value = res.address
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 立即更新时间
|
||||
updateTime()
|
||||
// 每秒更新时间
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
// 加载打卡信息
|
||||
loadPunchInfo()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除定时器
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.date-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 0 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.weekday-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-area {
|
||||
background: #f5f5f5;
|
||||
border-radius: 16rpx;
|
||||
padding: 80rpx 0;
|
||||
margin: 24rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200rpx;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 36rpx;
|
||||
color: #e34d59;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-text.status-punched {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.address-area {
|
||||
background: #f5f5f5;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.address-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.address-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.punch-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.punch-button {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 50%;
|
||||
background: #07c160;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(7, 193, 96, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.punch-button:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2rpx 10rpx rgba(7, 193, 96, 0.2);
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="位置选择打卡">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 当前位置信息 -->
|
||||
<view class="location-info-section">
|
||||
<view class="section-title">当前位置</view>
|
||||
<view class="location-card">
|
||||
<view class="location-item">
|
||||
<text class="label">地址:</text>
|
||||
<text class="value">{{ currentLocation.address || '正在获取...' }}</text>
|
||||
</view>
|
||||
<view class="location-item">
|
||||
<text class="label">经度:</text>
|
||||
<text class="value">{{ currentLocation.longitude || '--' }}</text>
|
||||
</view>
|
||||
<view class="location-item">
|
||||
<text class="label">纬度:</text>
|
||||
<text class="value">{{ currentLocation.latitude || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 位置列表 -->
|
||||
<view class="location-list-section">
|
||||
<view class="section-title">选择打卡位置</view>
|
||||
<scroll-view class="location-list" scroll-y>
|
||||
<view
|
||||
v-for="(location, index) in locationList"
|
||||
:key="index"
|
||||
class="location-item-card"
|
||||
:class="{ active: selectedLocationIndex === index }"
|
||||
@tap="handleSelectLocation(index)"
|
||||
>
|
||||
<view class="location-item-header">
|
||||
<text class="location-name">{{ location.name }}</text>
|
||||
<up-icon
|
||||
v-if="selectedLocationIndex === index"
|
||||
name="checkmark-circle-fill"
|
||||
size="24"
|
||||
color="#07c160"
|
||||
/>
|
||||
</view>
|
||||
<text class="location-address">{{ location.address }}</text>
|
||||
<text class="location-distance">{{ location.distance }}</text>
|
||||
</view>
|
||||
<view v-if="locationList.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无可用位置</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
<view class="submit-section">
|
||||
<up-button
|
||||
text="确认打卡"
|
||||
type="primary"
|
||||
:customStyle="submitButtonStyle"
|
||||
:disabled="selectedLocationIndex === null"
|
||||
@tap="handleConfirmPunch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import { getContentStyle, getSafeAreaInfo } from '@/utils/safeArea'
|
||||
|
||||
/**
|
||||
* 位置选择打卡页面
|
||||
* 业务背景:用于选择打卡位置并完成打卡操作
|
||||
* 设计决策:
|
||||
* 1. 显示当前位置信息(地址、经纬度)
|
||||
* 2. 提供位置列表供用户选择
|
||||
* 3. 支持确认打卡操作
|
||||
* 4. 使用scroll-view实现位置列表滚动
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: false, // 底部有按钮,不需要额外padding
|
||||
})
|
||||
})
|
||||
|
||||
// 当前位置信息
|
||||
const currentLocation = ref({
|
||||
address: '',
|
||||
longitude: '',
|
||||
latitude: '',
|
||||
})
|
||||
|
||||
// 位置列表
|
||||
const locationList = ref([
|
||||
{
|
||||
name: '办公大楼',
|
||||
address: '北京市朝阳区xxx街道xxx号',
|
||||
distance: '距离您 120m',
|
||||
},
|
||||
{
|
||||
name: '施工现场A区',
|
||||
address: '北京市朝阳区xxx街道xxx号',
|
||||
distance: '距离您 500m',
|
||||
},
|
||||
{
|
||||
name: '施工现场B区',
|
||||
address: '北京市朝阳区xxx街道xxx号',
|
||||
distance: '距离您 800m',
|
||||
},
|
||||
])
|
||||
|
||||
// 选中的位置索引
|
||||
const selectedLocationIndex = ref(null)
|
||||
|
||||
// 提交按钮样式
|
||||
const submitButtonStyle = computed(() => {
|
||||
const { safeAreaBottom } = getSafeAreaInfo()
|
||||
return {
|
||||
backgroundColor: '#07c160',
|
||||
borderColor: '#07c160',
|
||||
width: '100%',
|
||||
height: '88rpx',
|
||||
fontSize: '32rpx',
|
||||
borderRadius: '8rpx',
|
||||
marginBottom: `${safeAreaBottom}px`,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取当前位置
|
||||
*/
|
||||
const getCurrentLocation = () => {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
currentLocation.value = {
|
||||
address: '正在解析地址...',
|
||||
longitude: res.longitude.toFixed(6),
|
||||
latitude: res.latitude.toFixed(6),
|
||||
}
|
||||
// TODO: 调用逆地理编码接口获取地址
|
||||
// reverseGeocode(res.longitude, res.latitude)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('获取位置失败:', err)
|
||||
uni.showToast({
|
||||
title: '获取位置失败',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载位置列表
|
||||
*/
|
||||
const loadLocationList = () => {
|
||||
// TODO: 调用接口获取位置列表
|
||||
// const res = await getLocationList()
|
||||
// locationList.value = res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理选择位置
|
||||
* @param {Number} index - 位置索引
|
||||
*/
|
||||
const handleSelectLocation = (index) => {
|
||||
selectedLocationIndex.value = index
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理确认打卡
|
||||
*/
|
||||
const handleConfirmPunch = () => {
|
||||
if (selectedLocationIndex.value === null) {
|
||||
uni.showToast({
|
||||
title: '请选择打卡位置',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const selectedLocation = locationList.value[selectedLocationIndex.value]
|
||||
// TODO: 调用打卡接口
|
||||
console.log('打卡信息:', {
|
||||
location: selectedLocation,
|
||||
currentLocation: currentLocation.value,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '打卡成功',
|
||||
icon: 'success',
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取当前位置
|
||||
getCurrentLocation()
|
||||
// 加载位置列表
|
||||
loadLocationList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.location-info-section {
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.location-card {
|
||||
background: #f5f7fa;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.location-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.location-list-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.location-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.location-item-card {
|
||||
background: #f5f7fa;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
border-color: #07c160;
|
||||
background: rgba(7, 193, 96, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.location-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.location-name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.location-address {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.location-distance {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="考勤统计">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 日历区域 -->
|
||||
<view class="calendar-section">
|
||||
<!-- 月份导航 -->
|
||||
<view class="month-header">
|
||||
<view class="month-nav-btn" @tap="handlePrevMonth">
|
||||
<up-icon name="arrow-left" size="20" color="#333" />
|
||||
</view>
|
||||
<text class="month-text">{{ currentMonthText }}</text>
|
||||
<view class="month-nav-btn" @tap="handleNextMonth">
|
||||
<up-icon name="arrow-right" size="20" color="#333" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 星期标题 -->
|
||||
<view class="weekdays-header">
|
||||
<view v-for="(day, index) in weekdays" :key="index" class="weekday-cell">
|
||||
<text class="weekday-text">{{ day }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历网格 -->
|
||||
<view class="calendar-grid">
|
||||
<view
|
||||
v-for="(date, index) in calendarDays"
|
||||
:key="index"
|
||||
class="date-cell"
|
||||
:class="{
|
||||
'other-month': !date.isCurrentMonth,
|
||||
selected: date.isSelected,
|
||||
'has-abnormal': date.hasAbnormal,
|
||||
}"
|
||||
@tap="handleDateClick(date)"
|
||||
>
|
||||
<text class="date-number">{{ date.day }}</text>
|
||||
<text v-if="date.hasAbnormal" class="abnormal-label">异常</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期详情区域 -->
|
||||
<view class="detail-section">
|
||||
<text class="detail-date">{{ selectedDateText }}</text>
|
||||
<view class="detail-content">
|
||||
<view class="detail-line"></view>
|
||||
<text class="detail-status">{{ selectedDateStatus }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* 自有考勤统计页面
|
||||
* 业务背景:用于查看自有人员的考勤统计数据
|
||||
* 设计决策:
|
||||
* 1. 使用日历视图展示考勤数据
|
||||
* 2. 支持月份切换
|
||||
* 3. 标记异常日期
|
||||
* 4. 显示选中日期的考勤详情
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: true,
|
||||
})
|
||||
})
|
||||
|
||||
// 星期标题
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
|
||||
// 当前显示的月份
|
||||
const currentMonth = ref(dayjs())
|
||||
|
||||
// 选中的日期
|
||||
const selectedDate = ref(dayjs())
|
||||
|
||||
// 异常日期列表(示例数据,后续从接口获取)
|
||||
const abnormalDates = ref([
|
||||
'2025-11-30',
|
||||
'2025-12-01',
|
||||
// 可以添加更多异常日期
|
||||
])
|
||||
|
||||
// 日期状态映射(示例数据,后续从接口获取)
|
||||
const dateStatusMap = ref({
|
||||
'2025-12-01': '上班未打卡',
|
||||
'2025-11-30': '下班未打卡',
|
||||
// 可以添加更多日期状态
|
||||
})
|
||||
|
||||
// 当前月份文本
|
||||
const currentMonthText = computed(() => {
|
||||
return currentMonth.value.format('YYYY年MM月')
|
||||
})
|
||||
|
||||
// 选中的日期文本
|
||||
const selectedDateText = computed(() => {
|
||||
return selectedDate.value.format('YYYY年MM月DD日')
|
||||
})
|
||||
|
||||
// 选中日期的状态
|
||||
const selectedDateStatus = computed(() => {
|
||||
const dateKey = selectedDate.value.format('YYYY-MM-DD')
|
||||
return dateStatusMap.value[dateKey] || '正常'
|
||||
})
|
||||
|
||||
// 日历天数数组
|
||||
const calendarDays = computed(() => {
|
||||
const days = []
|
||||
const startOfMonth = currentMonth.value.startOf('month')
|
||||
const endOfMonth = currentMonth.value.endOf('month')
|
||||
const startDay = startOfMonth.day() // 0-6, 0是星期日
|
||||
|
||||
// 上个月的日期
|
||||
for (let i = startDay - 1; i >= 0; i--) {
|
||||
const date = dayjs(startOfMonth).subtract(i + 1, 'day')
|
||||
const dateKey = date.format('YYYY-MM-DD')
|
||||
days.push({
|
||||
date: date.toDate(),
|
||||
day: date.date(),
|
||||
isCurrentMonth: false,
|
||||
isSelected: false,
|
||||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||||
})
|
||||
}
|
||||
|
||||
// 当前月的日期
|
||||
for (let i = 0; i < endOfMonth.date(); i++) {
|
||||
const date = dayjs(startOfMonth).add(i, 'day')
|
||||
const dateKey = date.format('YYYY-MM-DD')
|
||||
const isSelected = date.isSame(selectedDate.value, 'day')
|
||||
days.push({
|
||||
date: date.toDate(),
|
||||
day: date.date(),
|
||||
isCurrentMonth: true,
|
||||
isSelected,
|
||||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||||
})
|
||||
}
|
||||
|
||||
// 下个月的日期(补齐到42个格子,6行x7列)
|
||||
const remainingDays = 42 - days.length
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
const date = dayjs(endOfMonth).add(i, 'day')
|
||||
const dateKey = date.format('YYYY-MM-DD')
|
||||
days.push({
|
||||
date: date.toDate(),
|
||||
day: date.date(),
|
||||
isCurrentMonth: false,
|
||||
isSelected: false,
|
||||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||||
})
|
||||
}
|
||||
|
||||
return days
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理日期点击
|
||||
* @param {Object} dateObj - 日期对象
|
||||
*/
|
||||
const handleDateClick = (dateObj) => {
|
||||
selectedDate.value = dayjs(dateObj.date)
|
||||
// TODO: 加载该日期的详细考勤信息
|
||||
loadDateDetail(dateObj.date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到上一个月
|
||||
*/
|
||||
const handlePrevMonth = () => {
|
||||
currentMonth.value = dayjs(currentMonth.value).subtract(1, 'month')
|
||||
// TODO: 重新加载该月的考勤数据
|
||||
loadMonthData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到下一个月
|
||||
*/
|
||||
const handleNextMonth = () => {
|
||||
currentMonth.value = dayjs(currentMonth.value).add(1, 'month')
|
||||
// TODO: 重新加载该月的考勤数据
|
||||
loadMonthData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载月份数据
|
||||
*/
|
||||
const loadMonthData = () => {
|
||||
// TODO: 调用接口获取该月的考勤数据
|
||||
// const res = await getAttendanceData(currentMonth.value.format('YYYY-MM'))
|
||||
// abnormalDates.value = res.abnormalDates
|
||||
// dateStatusMap.value = res.dateStatusMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载日期详情
|
||||
* @param {Date} date - 日期对象
|
||||
*/
|
||||
const loadDateDetail = (date) => {
|
||||
// TODO: 调用接口获取该日期的详细考勤信息
|
||||
// const res = await getDateDetail(dayjs(date).format('YYYY-MM-DD'))
|
||||
// 更新日期状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认选中今天
|
||||
selectedDate.value = dayjs()
|
||||
// 加载当前月数据
|
||||
loadMonthData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.calendar-section {
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.month-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.month-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: #f5f5f5;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.month-nav-btn:active {
|
||||
background: #e5e5e5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.month-text {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.weekdays-header {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.weekday-cell {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.weekday-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.date-cell {
|
||||
width: calc(100% / 7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16rpx 0;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.date-number {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.date-cell.other-month .date-number {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.date-cell.selected {
|
||||
background: #07c160;
|
||||
// border-radius: 50%;
|
||||
// width: 64rpx;
|
||||
// height: 64rpx;
|
||||
}
|
||||
|
||||
.date-cell.selected .date-number {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.abnormal-label {
|
||||
font-size: 20rpx;
|
||||
color: #e34d59;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.date-cell.selected .abnormal-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.detail-date {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 24rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #f5f5f5;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-line {
|
||||
width: 4rpx;
|
||||
height: 100%;
|
||||
background: #e34d59;
|
||||
border-radius: 2rpx;
|
||||
margin-right: 24rpx;
|
||||
position: absolute;
|
||||
left: 32rpx;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.detail-status {
|
||||
font-size: 28rpx;
|
||||
color: #e34d59;
|
||||
margin-left: 28rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="电子合同">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 合同列表 -->
|
||||
<scroll-view class="contract-list" scroll-y>
|
||||
<ReviewEmptyState v-if="contractList.length === 0" text="暂无合同" />
|
||||
<view v-else class="contract-items">
|
||||
<view
|
||||
v-for="(contract, index) in contractList"
|
||||
:key="index"
|
||||
class="contract-item"
|
||||
@tap="handleContractClick(contract)"
|
||||
>
|
||||
<view class="contract-header">
|
||||
<text class="contract-title">{{ contract.title }}</text>
|
||||
<text class="contract-status" :class="getStatusClass(contract.status)">
|
||||
{{ contract.status }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="contract-date">
|
||||
<text class="date-text">{{ contract.dateRange }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import ReviewEmptyState from '@/components/ReviewEmptyState/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
|
||||
/**
|
||||
* 自有电子合同页面
|
||||
* 业务背景:用于查看和管理自有人员的电子合同
|
||||
* 设计决策:
|
||||
* 1. 显示合同列表,包含合同标题、日期范围、状态
|
||||
* 2. 支持点击查看合同详情
|
||||
* 3. 使用scroll-view实现列表滚动
|
||||
* 4. 空状态友好提示
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: true,
|
||||
})
|
||||
})
|
||||
|
||||
// 合同列表数据
|
||||
const contractList = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '(驾驶员) 劳动合同',
|
||||
dateRange: '2025-03-01 ~ 2026-02-28',
|
||||
status: '有效',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '(技术员) 劳动合同',
|
||||
dateRange: '2024-06-01 ~ 2025-05-31',
|
||||
status: '有效',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '(安全员) 劳动合同',
|
||||
dateRange: '2023-01-01 ~ 2023-12-31',
|
||||
status: '已过期',
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* 获取状态样式类
|
||||
* @param {String} status - 状态值
|
||||
* @returns {String} 样式类名
|
||||
*/
|
||||
const getStatusClass = (status) => {
|
||||
const statusMap = {
|
||||
有效: 'status-valid',
|
||||
已过期: 'status-expired',
|
||||
待签署: 'status-pending',
|
||||
}
|
||||
return statusMap[status] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理合同点击
|
||||
* @param {Object} contract - 合同对象
|
||||
*/
|
||||
const handleContractClick = (contract) => {
|
||||
// TODO: 跳转到合同详情页面
|
||||
console.log('查看合同详情:', contract)
|
||||
uni.showToast({
|
||||
title: '合同详情功能待完善',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载合同列表
|
||||
*/
|
||||
const loadContractList = () => {
|
||||
// TODO: 调用接口获取合同列表
|
||||
// const res = await getContractList()
|
||||
// contractList.value = res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 加载合同列表
|
||||
loadContractList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contract-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.contract-items {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.contract-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.contract-item:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.contract-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.contract-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contract-status {
|
||||
font-size: 26rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-valid {
|
||||
color: #07c160;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
.status-expired {
|
||||
color: #999;
|
||||
background: rgba(153, 153, 153, 0.1);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #ed7b2f;
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
}
|
||||
|
||||
.contract-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="公告">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<scroll-view class="detail-content" scroll-y>
|
||||
<!-- 消息头部 -->
|
||||
<view class="message-header">
|
||||
<view class="message-icon">
|
||||
<text class="icon-text">公告</text>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<text class="message-title">{{ messageDetail.title }}</text>
|
||||
<text class="message-time">{{ messageDetail.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<view class="message-body">
|
||||
<text class="body-title">{{ messageDetail.title }}</text>
|
||||
<view class="body-content">
|
||||
<text class="content-text">{{ messageDetail.content }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 图片列表 -->
|
||||
<view
|
||||
v-if="messageDetail.images && messageDetail.images.length > 0"
|
||||
class="image-list"
|
||||
>
|
||||
<image
|
||||
v-for="(image, index) in messageDetail.images"
|
||||
:key="index"
|
||||
:src="image"
|
||||
mode="widthFix"
|
||||
class="content-image"
|
||||
@tap="handleImagePreview(index)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 确认按钮区域 -->
|
||||
<view class="confirm-section">
|
||||
<up-button
|
||||
:text="confirmButtonText"
|
||||
type="primary"
|
||||
:customStyle="confirmButtonStyle"
|
||||
:disabled="!canConfirm"
|
||||
@tap="handleConfirm"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import { getContentStyle, getSafeAreaInfo } from '@/utils/safeArea'
|
||||
|
||||
/**
|
||||
* 消息详情页面
|
||||
* 业务背景:用于查看消息的详细内容
|
||||
* 设计决策:
|
||||
* 1. 显示消息标题、时间、内容
|
||||
* 2. 支持图片预览
|
||||
* 3. 底部有5秒倒计时按钮,倒计时结束后显示"点击确认"
|
||||
* 4. 使用scroll-view实现内容滚动
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: false, // 底部有按钮,不需要额外padding
|
||||
})
|
||||
})
|
||||
|
||||
// 消息ID
|
||||
const messageId = ref('')
|
||||
// 倒计时
|
||||
const countdown = ref(5)
|
||||
// 倒计时定时器
|
||||
let countdownTimer = null
|
||||
// 是否可以确认
|
||||
const canConfirm = ref(false)
|
||||
|
||||
// 消息详情数据
|
||||
const messageDetail = ref({
|
||||
id: 1,
|
||||
title: '关于内部招聘安全应急值班员的通知',
|
||||
time: '2024-10-12 13:49:32',
|
||||
content: `根据公司目前用工需求,现面向新解文传务造和防务承医员工开展内部招聘,招聘应急值班员2名。
|
||||
|
||||
一、任职资格及条件
|
||||
1. 男女不限,年龄30岁及以下
|
||||
2. 具备电力系统业务知识
|
||||
3. 熟悉企急管理相关工作
|
||||
4. 熟练使用PMS(备部系统)和新一代高静(系)
|
||||
5. 具备数据统计和分析能力,具备文字、数级、图表编辑能力
|
||||
|
||||
二、应急信员工作职责
|
||||
1. 急预警响应
|
||||
2. 传达上级领导有关工作指示和要求
|
||||
3. 完成领导成有关部门交办的相关临时工作任务
|
||||
|
||||
三、值班时间和地点
|
||||
1. 每天安排人洪堡 (09:00 09:00, 24小时)
|
||||
2. '上一休二'模式
|
||||
3. 地点: 国网湖南省电力有限公司应急指挥中心
|
||||
|
||||
四、将程序
|
||||
1. 符合条件的员工自同报名,免电话联系
|
||||
2. 再将个人阴历次阅
|
||||
3. 邮箱: 177528415000163.com
|
||||
4. 截止时间: 2024年10月31日
|
||||
5. 联系人: 刘香 电话: 17752841509`,
|
||||
images: [
|
||||
// 示例图片URL,实际使用时替换为真实图片
|
||||
// 'https://example.com/image1.jpg',
|
||||
],
|
||||
})
|
||||
|
||||
// 确认按钮文本
|
||||
const confirmButtonText = computed(() => {
|
||||
if (countdown.value > 0) {
|
||||
return `${countdown.value}秒`
|
||||
}
|
||||
return '点击确认'
|
||||
})
|
||||
|
||||
// 确认按钮样式
|
||||
const confirmButtonStyle = computed(() => {
|
||||
const { safeAreaBottom } = getSafeAreaInfo()
|
||||
return {
|
||||
backgroundColor: canConfirm.value ? '#07c160' : '#ccc',
|
||||
borderColor: canConfirm.value ? '#07c160' : '#ccc',
|
||||
width: '100%',
|
||||
height: '88rpx',
|
||||
fontSize: '32rpx',
|
||||
borderRadius: '8rpx',
|
||||
marginBottom: `${safeAreaBottom}px`,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 开始倒计时
|
||||
*/
|
||||
const startCountdown = () => {
|
||||
countdown.value = 5
|
||||
canConfirm.value = false
|
||||
|
||||
countdownTimer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
canConfirm.value = true
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图片预览
|
||||
* @param {Number} index - 图片索引
|
||||
*/
|
||||
const handleImagePreview = (index) => {
|
||||
if (!messageDetail.value.images || messageDetail.value.images.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
uni.previewImage({
|
||||
urls: messageDetail.value.images,
|
||||
current: index,
|
||||
fail: (err) => {
|
||||
console.error('预览图片失败:', err)
|
||||
uni.showToast({
|
||||
title: '预览图片失败',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理确认
|
||||
*/
|
||||
const handleConfirm = () => {
|
||||
if (!canConfirm.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 调用接口确认已读
|
||||
console.log('确认已读消息:', messageId.value)
|
||||
uni.showToast({
|
||||
title: '已确认',
|
||||
icon: 'success',
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载消息详情
|
||||
*/
|
||||
const loadMessageDetail = () => {
|
||||
// TODO: 根据messageId调用接口获取消息详情
|
||||
// const res = await getMessageDetail(messageId.value)
|
||||
// messageDetail.value = res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取消息ID
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = currentPage.options || {}
|
||||
messageId.value = options.id || ''
|
||||
} catch (error) {
|
||||
console.error('获取页面参数失败:', error)
|
||||
}
|
||||
|
||||
// 加载消息详情
|
||||
loadMessageDetail()
|
||||
// 开始倒计时
|
||||
startCountdown()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除定时器
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: #07c160;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.body-title {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 32rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.body-content {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.content-image {
|
||||
width: 100%;
|
||||
border-radius: 8rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.confirm-section {
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="消息中心">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view class="message-list" scroll-y>
|
||||
<ReviewEmptyState v-if="messageList.length === 0" text="暂无消息" />
|
||||
<view v-else class="message-items">
|
||||
<view
|
||||
v-for="(message, index) in messageList"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
@tap="handleMessageClick(message)"
|
||||
>
|
||||
<view class="message-icon">
|
||||
<text class="icon-text">公告</text>
|
||||
</view>
|
||||
<view class="message-content">
|
||||
<text class="message-title">{{ message.title }}</text>
|
||||
<text class="message-desc">{{ message.desc }}</text>
|
||||
</view>
|
||||
<text class="message-time">{{ message.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import ReviewEmptyState from '@/components/ReviewEmptyState/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
|
||||
/**
|
||||
* 自有消息通知页面
|
||||
* 业务背景:用于查看自有人员的消息通知
|
||||
* 设计决策:
|
||||
* 1. 显示消息列表,包含图标、标题、描述、时间
|
||||
* 2. 支持点击查看消息详情
|
||||
* 3. 使用scroll-view实现列表滚动
|
||||
* 4. 空状态友好提示
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: true,
|
||||
})
|
||||
})
|
||||
|
||||
// 消息列表数据
|
||||
const messageList = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '湖南新麒立人力资源公司关于招聘安全应急值班员的通知',
|
||||
desc: '根据公司目前用工需求,现面向新…',
|
||||
time: '2025-05-13 10:21:53',
|
||||
content: '根据公司目前用工需求,现面向新解文传务造和防务承医员工开展内部招聘...',
|
||||
images: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '关于内部招聘安全应急值班员的通知',
|
||||
desc: '关于内部招聘安全应急值班员的通知',
|
||||
time: '2024-10-12 13:49:32',
|
||||
content: '关于内部招聘安全应急值班员的通知...',
|
||||
images: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '关于招聘安全应急值班员的通知',
|
||||
desc: '关于招聘安全应急值班员的通知',
|
||||
time: '2024-10-12 13:47:32',
|
||||
content: '关于招聘安全应急值班员的通知...',
|
||||
images: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '员工职称及技能等级评定通道的相关说明',
|
||||
desc: '员工职称及技能等级评定通道的相关…',
|
||||
time: '2024-10-10 10:02:48',
|
||||
content: '员工职称及技能等级评定通道的相关说明...',
|
||||
images: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '自有人员系统 续签合同的操作步骤',
|
||||
desc: '自有人员系统 续签合同的操作步骤…',
|
||||
time: '2024-04-15 15:58:05',
|
||||
content: '自有人员系统 续签合同的操作步骤...',
|
||||
images: [],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '关于机具设备分公司社会化用工招聘的通知',
|
||||
desc: '关于机具设备分公司社会化用工招聘…',
|
||||
time: '2023-11-17 15:24:38',
|
||||
content: '关于机具设备分公司社会化用工招聘的通知...',
|
||||
images: [],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: '关于机具设备分公司社会化用工招聘的通知',
|
||||
desc: '关于机具设备分公司社会化用工招聘…',
|
||||
time: '2023-11-17 15:23:06',
|
||||
content: '关于机具设备分公司社会化用工招聘的通知...',
|
||||
images: [],
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
* 处理消息点击
|
||||
* @param {Object} message - 消息对象
|
||||
*/
|
||||
const handleMessageClick = (message) => {
|
||||
// 跳转到消息详情页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/own/message-notification/detail/index?id=${message.id}`,
|
||||
fail: (err) => {
|
||||
console.error('导航失败:', err)
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载消息列表
|
||||
*/
|
||||
const loadMessageList = () => {
|
||||
// TODO: 调用接口获取消息列表
|
||||
// const res = await getMessageList()
|
||||
// messageList.value = res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 加载消息列表
|
||||
loadMessageList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.message-items {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.message-item:active {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: #07c160;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="工资条">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<scroll-view class="detail-content" scroll-y>
|
||||
<!-- 头部信息 -->
|
||||
<view class="header-section">
|
||||
<text class="month-text">{{ payslipDetail.month }}实发</text>
|
||||
<text class="amount-text">{{ payslipDetail.netPay }}</text>
|
||||
<text class="gratitude-text">感谢为公司的付出,辛苦了!</text>
|
||||
</view>
|
||||
|
||||
<!-- 摘要信息 -->
|
||||
<view class="summary-section">
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">应发工资</text>
|
||||
<text class="summary-value">{{ payslipDetail.grossSalary }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">个人所得税</text>
|
||||
<text class="summary-value">{{ payslipDetail.incomeTax }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">公积金</text>
|
||||
<text class="summary-value">{{ payslipDetail.housingFund }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收入明细 -->
|
||||
<view class="detail-section">
|
||||
<view class="section-title">收入明细</view>
|
||||
<view class="detail-list">
|
||||
<view
|
||||
v-for="(item, index) in payslipDetail.earnings"
|
||||
:key="index"
|
||||
class="detail-row"
|
||||
:class="{ 'row-even': index % 2 === 1 }"
|
||||
>
|
||||
<text class="detail-label">{{ item.label }}</text>
|
||||
<text class="detail-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 扣除明细 -->
|
||||
<view class="detail-section">
|
||||
<view class="section-title">扣除明细</view>
|
||||
<view class="detail-list">
|
||||
<view
|
||||
v-for="(item, index) in payslipDetail.deductions"
|
||||
:key="index"
|
||||
class="detail-row"
|
||||
:class="{ 'row-even': index % 2 === 1 }"
|
||||
>
|
||||
<text class="detail-label">{{ item.label }}</text>
|
||||
<text class="detail-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最终汇总 -->
|
||||
<view class="final-section">
|
||||
<view class="final-row">
|
||||
<text class="final-label">合计</text>
|
||||
<text class="final-value">{{ payslipDetail.totalDeductions }}</text>
|
||||
</view>
|
||||
<view class="final-row">
|
||||
<text class="final-label">税前金额</text>
|
||||
<text class="final-value">{{ payslipDetail.preTaxAmount }}</text>
|
||||
</view>
|
||||
<view class="final-row">
|
||||
<text class="final-label">个税</text>
|
||||
<text class="final-value">{{ payslipDetail.incomeTax }}</text>
|
||||
</view>
|
||||
<view class="final-row">
|
||||
<text class="final-label">扣保费</text>
|
||||
<text class="final-value">{{ payslipDetail.insuranceDeduction }}</text>
|
||||
</view>
|
||||
<view class="final-row highlight">
|
||||
<text class="final-label">实发金额</text>
|
||||
<text class="final-value highlight">{{ payslipDetail.netPay }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* 工资条详情页面
|
||||
* 业务背景:用于查看工资条的详细信息
|
||||
* 设计决策:
|
||||
* 1. 显示月份和实发金额
|
||||
* 2. 显示感谢语
|
||||
* 3. 显示摘要信息(应发工资、个税、公积金)
|
||||
* 4. 显示详细的收入明细和扣除明细
|
||||
* 5. 显示最终汇总信息
|
||||
* 6. 使用scroll-view实现内容滚动
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: true,
|
||||
})
|
||||
})
|
||||
|
||||
// 年份和月份
|
||||
const selectedYear = ref(2025)
|
||||
const selectedMonth = ref(1)
|
||||
|
||||
// 工资条详情数据
|
||||
const payslipDetail = ref({
|
||||
month: '2025年01月',
|
||||
netPay: '4957.27',
|
||||
grossSalary: '6077',
|
||||
incomeTax: '312.11',
|
||||
housingFund: '154',
|
||||
earnings: [
|
||||
{ label: '基本工资/岗位薪点工资', value: '1930' },
|
||||
{ label: '月度绩效/考核工资', value: '2040' },
|
||||
{ label: '季度绩效', value: '0' },
|
||||
{ label: '年度绩效', value: '1840' },
|
||||
{ label: '定额工资', value: '0' },
|
||||
{ label: '加班工资', value: '267' },
|
||||
{ label: '计件工资', value: '0' },
|
||||
{ label: '节日补贴', value: '0' },
|
||||
{ label: '伙食补贴', value: '0' },
|
||||
{ label: '持证津贴', value: '0' },
|
||||
{ label: '高温补贴', value: '0' },
|
||||
{ label: '夜间补贴', value: '0' },
|
||||
{ label: '女职工卫生费', value: '0' },
|
||||
{ label: '安全奖', value: '0' },
|
||||
{ label: '质量奖', value: '0' },
|
||||
{ label: '优秀项目奖', value: '0' },
|
||||
{ label: '月度公里数', value: '0' },
|
||||
{ label: '月度综合考评先进奖励', value: '0' },
|
||||
{ label: '表扬奖励', value: '0' },
|
||||
{ label: '其他', value: '0' },
|
||||
],
|
||||
deductions: [
|
||||
{ label: '安全罚款', value: '0' },
|
||||
{ label: '质量罚款', value: '0' },
|
||||
{ label: '其他扣款', value: '0' },
|
||||
{ label: '应发工资', value: '6077' },
|
||||
{ label: '养老', value: '344.64' },
|
||||
{ label: '医疗', value: '81.06' },
|
||||
{ label: '失业', value: '12.92' },
|
||||
{ label: '大病', value: '15' },
|
||||
{ label: '社保补差', value: '0' },
|
||||
{ label: '公积金', value: '154' },
|
||||
{ label: '公积金补差', value: '0' },
|
||||
],
|
||||
totalDeductions: '607.62',
|
||||
preTaxAmount: '5469.38',
|
||||
insuranceDeduction: '200',
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载工资条详情
|
||||
*/
|
||||
const loadPayslipDetail = () => {
|
||||
// TODO: 根据年份和月份调用接口获取工资条详情
|
||||
// const res = await getPayslipDetail(selectedYear.value, selectedMonth.value)
|
||||
// payslipDetail.value = res.data
|
||||
// 更新月份显示
|
||||
payslipDetail.value.month = `${selectedYear.value}年${String(selectedMonth.value).padStart(2, '0')}月`
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取年份和月份参数
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const options = currentPage.options || {}
|
||||
selectedYear.value = parseInt(options.year) || dayjs().year()
|
||||
selectedMonth.value = parseInt(options.month) || dayjs().month() + 1
|
||||
} catch (error) {
|
||||
console.error('获取页面参数失败:', error)
|
||||
}
|
||||
|
||||
// 加载工资条详情
|
||||
loadPayslipDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
background: linear-gradient(180deg, #07c160 0%, #06b050 100%);
|
||||
padding: 48rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.month-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
font-size: 72rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.gratitude-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
background: #fff;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: 32rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.detail-row.row-even {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
.final-section {
|
||||
margin: 32rpx;
|
||||
padding: 32rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.final-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.final-row.highlight {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 2rpx solid #07c160;
|
||||
}
|
||||
|
||||
.final-label {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.final-value {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.final-value.highlight {
|
||||
font-size: 36rpx;
|
||||
color: #07c160;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="工资条">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
<template #right>
|
||||
<view class="year-selector" @tap="handleOpenYearPicker">
|
||||
<text class="year-text">{{ selectedYear }}</text>
|
||||
<up-icon name="arrow-down" size="16" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 工资条列表 -->
|
||||
<scroll-view class="payslip-list" scroll-y>
|
||||
<ReviewEmptyState v-if="payslipList.length === 0" text="暂无工资条" />
|
||||
<view v-else class="payslip-items">
|
||||
<view
|
||||
v-for="(payslip, index) in payslipList"
|
||||
:key="index"
|
||||
class="payslip-item"
|
||||
@tap="handlePayslipClick(payslip)"
|
||||
>
|
||||
<text class="month-label">{{ payslip.month }}</text>
|
||||
<view class="amount-card">
|
||||
<text class="amount-text">{{ payslip.amount }}</text>
|
||||
<up-icon name="arrow-right" size="20" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 年份选择器 -->
|
||||
<up-picker
|
||||
ref="yearPickerRef"
|
||||
:show="showYearPicker"
|
||||
:columns="yearColumns"
|
||||
closeOnClickOverlay
|
||||
@cancel="showYearPicker = false"
|
||||
@confirm="handleYearConfirm"
|
||||
@close="showYearPicker = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import ReviewEmptyState from '@/components/ReviewEmptyState/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* 自有工资条页面
|
||||
* 业务背景:用于查看自有人员的工资条信息
|
||||
* 设计决策:
|
||||
* 1. 显示按月份排列的工资条列表
|
||||
* 2. 支持年份选择
|
||||
* 3. 支持点击查看工资条详情
|
||||
* 4. 使用scroll-view实现列表滚动
|
||||
*/
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
includeStatusBar: true,
|
||||
includeBottomSafeArea: true,
|
||||
})
|
||||
})
|
||||
|
||||
// 选中的年份
|
||||
const selectedYear = ref(dayjs().year())
|
||||
const showYearPicker = ref(false)
|
||||
const yearPickerRef = ref(null)
|
||||
|
||||
// 工资条列表数据
|
||||
const payslipList = ref([
|
||||
{ month: '2025年01月', amount: '4957.27', year: 2025, monthNum: 1 },
|
||||
{ month: '2025年02月', amount: '3521.84', year: 2025, monthNum: 2 },
|
||||
{ month: '2025年03月', amount: '3677.28', year: 2025, monthNum: 3 },
|
||||
{ month: '2025年04月', amount: '7764.28', year: 2025, monthNum: 4 },
|
||||
{ month: '2025年05月', amount: '8165.06', year: 2025, monthNum: 5 },
|
||||
{ month: '2025年06月', amount: '7234.56', year: 2025, monthNum: 6 },
|
||||
{ month: '2025年07月', amount: '6892.34', year: 2025, monthNum: 7 },
|
||||
{ month: '2025年08月', amount: '7456.78', year: 2025, monthNum: 8 },
|
||||
{ month: '2025年09月', amount: '8123.45', year: 2025, monthNum: 9 },
|
||||
{ month: '2025年10月', amount: '7890.12', year: 2025, monthNum: 10 },
|
||||
{ month: '2025年11月', amount: '8567.89', year: 2025, monthNum: 11 },
|
||||
{ month: '2025年12月', amount: '9234.56', year: 2025, monthNum: 12 },
|
||||
])
|
||||
|
||||
// 年份选项(生成最近5年)
|
||||
const yearOptions = computed(() => {
|
||||
const currentYear = dayjs().year()
|
||||
const years = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
years.push(currentYear - i)
|
||||
}
|
||||
return years
|
||||
})
|
||||
|
||||
// 年份选择器columns
|
||||
const yearColumns = computed(() => {
|
||||
return [
|
||||
yearOptions.value.map((year) => ({
|
||||
text: `${year}年`,
|
||||
value: year,
|
||||
})),
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* 打开年份选择器
|
||||
*/
|
||||
const handleOpenYearPicker = () => {
|
||||
showYearPicker.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认选择年份
|
||||
* @param {Object} e - 事件对象
|
||||
*/
|
||||
const handleYearConfirm = (e) => {
|
||||
selectedYear.value = e.value[0]
|
||||
showYearPicker.value = false
|
||||
// TODO: 根据年份重新加载工资条列表
|
||||
loadPayslipList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理工资条点击
|
||||
* @param {Object} payslip - 工资条对象
|
||||
*/
|
||||
const handlePayslipClick = (payslip) => {
|
||||
// 跳转到工资条详情页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/own/payslip/detail/index?year=${payslip.year}&month=${payslip.monthNum}`,
|
||||
fail: (err) => {
|
||||
console.error('导航失败:', err)
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载工资条列表
|
||||
*/
|
||||
const loadPayslipList = () => {
|
||||
// TODO: 调用接口获取工资条列表
|
||||
// const res = await getPayslipList(selectedYear.value)
|
||||
// payslipList.value = res.data
|
||||
// 过滤当前年份的工资条
|
||||
const filtered = payslipList.value.filter((item) => item.year === selectedYear.value)
|
||||
// 这里只是示例,实际应该从接口获取
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 加载工资条列表
|
||||
loadPayslipList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.payslip-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.payslip-items {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.payslip-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.month-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.amount-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.amount-card:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.year-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.year-selector:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.year-text {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -253,7 +253,7 @@
|
|||
<!-- 开始时间选择器 -->
|
||||
<up-datetime-picker
|
||||
ref="startTimePickerRef"
|
||||
mode="datetime"
|
||||
mode="date"
|
||||
:show="showStartTimePicker"
|
||||
v-model="startTimePickerValue"
|
||||
@cancel="showStartTimePicker = false"
|
||||
|
|
@ -263,7 +263,7 @@
|
|||
<!-- 结束时间选择器 -->
|
||||
<up-datetime-picker
|
||||
ref="endTimePickerRef"
|
||||
mode="datetime"
|
||||
mode="date"
|
||||
:show="showEndTimePicker"
|
||||
v-model="endTimePickerValue"
|
||||
@cancel="showEndTimePicker = false"
|
||||
|
|
@ -452,7 +452,7 @@ const submitButtonStyle = computed(() => {
|
|||
*/
|
||||
const formatDateTime = (timestamp) => {
|
||||
if (!timestamp) return ''
|
||||
return dayjs(timestamp).format('YYYY-MM-DD HH:mm')
|
||||
return dayjs(timestamp).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue