增加基础页面

This commit is contained in:
BianLzhaoMin 2025-12-01 16:51:14 +08:00
parent 705ffbe78c
commit 162f52a8c5
13 changed files with 3372 additions and 129 deletions

View File

@ -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": {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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),
})
}
// 426x7
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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')
}
/**