feat: 建筑数字化管理系统静态原型
Vue 3 + Element Plus + Vite,12 页面(PC 7 页 + 移动端 5 页), 暖色亲和视觉风格,手机模拟框小程序预览,完整模拟数据 + CRUD 操作。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
a6a3747327
5
20260515/.gitignore
vendored
Normal file
5
20260515/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.superpowers/
|
||||
.DS_Store
|
||||
*.log
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,165 @@
|
||||
# 建筑数字化管理系统 - 静态页面原型设计
|
||||
|
||||
**日期**: 2026-05-15
|
||||
**来源**: 2026-05-14 AI 会议纪要 (document.txt)
|
||||
|
||||
## 概述
|
||||
|
||||
面向中小建筑企业的轻量化项目数字化管理系统静态原型。覆盖考勤打卡、工时评分、项目生命周期管理、财务回款追踪四大核心模块。原型包含小程序端(手机模拟视图)和 PC 后台管理端,双端共用一套模拟数据。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: Vue 3 + Vite
|
||||
- **UI 库**: Element Plus(PC 端)
|
||||
- **路由**: Vue Router 4,PC 和移动端独立路由分组
|
||||
- **移动端展示**: CSS 手机模拟框,375x667 视口,移动端样式适配
|
||||
- **状态管理**: Reactive store,模拟数据内嵌,无需后端
|
||||
- **构建产物**: `vite build` 输出纯静态 HTML/CSS/JS,可部署至任意 HTTP 服务器
|
||||
|
||||
## 视觉风格
|
||||
|
||||
暖色亲和风格:
|
||||
- **主色调**: 琥珀/橙色系
|
||||
- **背景**: 暖白 / 浅奶油色
|
||||
- **卡片**: 圆角、柔和阴影
|
||||
- **字体**: 无衬线体,亲和但不失专业感
|
||||
- **定位**: 以人为本、工人视角优先,区别于传统冷冰冰的企业软件
|
||||
|
||||
## 页面清单(12 页)
|
||||
|
||||
### 手机端(5 页)—— 375x667 手机模拟框展示
|
||||
|
||||
| 序号 | 页面 | 核心内容 |
|
||||
|------|------|----------|
|
||||
| 1 | 登录页 | 手机号 + 验证码登录 |
|
||||
| 2 | 项目切换 | 所属项目列表,点击切换当前项目 |
|
||||
| 3 | 首页 | 项目名称/位置、今日考勤状态、评分环、快捷统计 |
|
||||
| 4 | 打卡页 | GPS 定位指示、早晚打卡按钮、评分实时反馈 |
|
||||
| 5 | 个人记录 | 考勤历史列表、每日工时、日薪结算 |
|
||||
|
||||
移动端导航:底部 4 个 Tab(首页、打卡、记录、我的)。
|
||||
|
||||
### PC 后台端(7 页)
|
||||
|
||||
| 序号 | 页面 | 核心内容 |
|
||||
|------|------|----------|
|
||||
| 1 | 登录页 | 账号密码登录表单 |
|
||||
| 2 | 仪表盘首页 | 统计卡片(项目数、在岗工人、出勤率、回款汇总)、项目概览表格、近期动态 |
|
||||
| 3 | 项目管理 | 项目列表(名称、位置、合同金额、回款进度)、新增/编辑弹窗、项目详情抽屉 |
|
||||
| 4 | 人员管理 | 工人列表(姓名、电话、所属项目)、增删人员、项目分配 |
|
||||
| 5 | 考勤管理 | 打卡记录表格(按日期/项目/人员筛选)、手动补卡弹窗、工时调整 |
|
||||
| 6 | 财务管理 | 回款追踪表格(合同金额、预付款到账、下笔款项日期、待收余额)、回款状态标签 |
|
||||
| 7 | 系统设置 | 管理员账号列表、角色配置、操作日志表、系统参数 |
|
||||
|
||||
PC 导航:深色侧边栏菜单(可折叠)+ 顶部栏(用户信息、手机预览按钮)。
|
||||
|
||||
## 布局结构
|
||||
|
||||
### PC 端布局
|
||||
```
|
||||
+-------+------------------------------------+
|
||||
| | 顶部栏(用户信息、通知、手机预览) |
|
||||
| 侧边 +------------------------------------+
|
||||
| 栏 | |
|
||||
| 菜单 | 内容区(面包屑 + 页面内容) |
|
||||
| | |
|
||||
+-------+------------------------------------+
|
||||
```
|
||||
|
||||
### 手机端布局(模拟框)
|
||||
```
|
||||
+------------------+
|
||||
| 状态栏 |
|
||||
| 页面标题栏 |
|
||||
| |
|
||||
| 内容区 |
|
||||
| |
|
||||
+------------------+
|
||||
| 底部 Tab 栏 |
|
||||
+------------------+
|
||||
```
|
||||
|
||||
### 双端切换方式
|
||||
PC 端顶部栏提供「手机预览」按钮,点击后在右侧滑出面板内展示 375x667 手机模拟框。手机预览与 PC 端共享同一套模拟数据源,操作联动。
|
||||
|
||||
## 模拟数据模型
|
||||
|
||||
所有数据以 JS 常量内嵌,CRUD 操作在内存中生效,页面刷新后重置。
|
||||
|
||||
### 项目
|
||||
```
|
||||
id, 项目名称, 项目位置, 合同金额, 预付款已收,
|
||||
下笔款项日期, 下笔款项金额, 待收余额, 状态
|
||||
```
|
||||
|
||||
### 工人
|
||||
```
|
||||
id, 姓名, 手机号, 所属项目ID, 入场日期, 状态
|
||||
```
|
||||
|
||||
### 考勤记录
|
||||
```
|
||||
id, 工人ID, 项目ID, 日期, 上班打卡时间, 下班打卡时间,
|
||||
评分(0-10), 工时(小时), 日薪, 状态(正常/手动调整)
|
||||
```
|
||||
|
||||
### 管理员
|
||||
```
|
||||
id, 用户名, 角色(超级管理员/管理员), 最后登录时间
|
||||
```
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
1. **无审批流程**: 管理员操作(手动补卡、工时调整、删除人员)直接生效。会议明确要求简化操作,不走多级审批。
|
||||
|
||||
2. **10 分制评分可视化**: 手机端首页和打卡页展示评分环。满勤 10 小时 = 10 分 = 100% 日薪。早退每小时扣 1 分,日薪按比例折算(如 100 元/天,9 分得 90 元)。
|
||||
|
||||
3. **GPS 定位为 UI 状态指示**: 打卡页展示位置状态徽章(绿色 = 在范围内,红色 = 超出范围)。实际定位逻辑用模拟开关替代,方便演示。
|
||||
|
||||
4. **项目全生命周期一表展示**: 财务管理页将合同金额、预付款、付款计划、待收余额整合在一张表中,无需多层下钻。
|
||||
|
||||
5. **模拟数据力求真实**: 使用真实感的中文建筑项目名称、工人姓名、符合会议提及规模的财务数据(百万级合同额、百人规模工人)。
|
||||
|
||||
## 组件结构
|
||||
|
||||
```
|
||||
src/
|
||||
router/
|
||||
index.js # Vue Router,PC 和移动端路由分组
|
||||
stores/
|
||||
index.js # Reactive store,模拟数据 + CRUD 方法
|
||||
views/
|
||||
pc/
|
||||
PcLogin.vue # PC 登录页
|
||||
Dashboard.vue # 仪表盘首页
|
||||
ProjectMgmt.vue # 项目管理
|
||||
WorkerMgmt.vue # 人员管理
|
||||
AttendanceMgmt.vue # 考勤管理
|
||||
FinanceMgmt.vue # 财务管理
|
||||
SystemSettings.vue # 系统设置
|
||||
mobile/
|
||||
MobileLogin.vue # 手机登录页
|
||||
ProjectSwitch.vue # 项目切换
|
||||
MobileHome.vue # 手机首页
|
||||
ClockIn.vue # 打卡页
|
||||
PersonalRecords.vue # 个人记录
|
||||
layouts/
|
||||
PcLayout.vue # PC 端侧边栏 + 顶部栏外壳
|
||||
MobileFrame.vue # 手机模拟框包装器
|
||||
components/
|
||||
ScoreRing.vue # 环形评分组件
|
||||
GpsBadge.vue # GPS 位置状态徽章
|
||||
StatCard.vue # 仪表盘统计卡片
|
||||
PhoneSimulator.vue # 375x667 手机框容器
|
||||
App.vue
|
||||
main.js
|
||||
```
|
||||
|
||||
## 不包含范围
|
||||
|
||||
- 真实登录认证 / Token 管理
|
||||
- 后端 API 对接
|
||||
- 真实 GPS 定位
|
||||
- 文件上传 / 文档管理
|
||||
- 数据持久化(刷新即重置)
|
||||
- PC 页面的响应式适配(固定桌面宽度)
|
||||
12
20260515/index.html
Normal file
12
20260515/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>建筑数字化管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1423
20260515/package-lock.json
generated
Normal file
1423
20260515/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
20260515/package.json
Normal file
20
20260515/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "construction-management-prototype",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0",
|
||||
"element-plus": "^2.7.0",
|
||||
"@element-plus/icons-vue": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.4.0"
|
||||
}
|
||||
}
|
||||
3
20260515/src/App.vue
Normal file
3
20260515/src/App.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
23
20260515/src/components/GpsBadge.vue
Normal file
23
20260515/src/components/GpsBadge.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<span :style="{
|
||||
display:'inline-flex', alignItems:'center', gap:'4px', padding:'4px 12px',
|
||||
borderRadius:'20px', fontSize:'12px',
|
||||
background: inRange ? '#f0f9eb' : '#fef0f0',
|
||||
color: inRange ? '#67c23a' : '#f56c6c',
|
||||
border: `1px solid ${inRange ? '#e1f3d8' : '#fde2e2'}`
|
||||
}">
|
||||
<span :style="{ width:'8px',height:'8px',borderRadius:'50%',background: inRange ? '#67c23a' : '#f56c6c',animation: inRange ? 'pulse 2s infinite' : 'none' }"></span>
|
||||
{{ inRange ? 'GPS 定位就绪' : '超出打卡范围' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ inRange: Boolean })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
</style>
|
||||
53
20260515/src/components/PhoneSimulator.vue
Normal file
53
20260515/src/components/PhoneSimulator.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="phone-simulator">
|
||||
<div class="phone-status-bar">9:41</div>
|
||||
<div class="phone-header">建筑管理系统</div>
|
||||
<div class="phone-content">
|
||||
<component :is="currentComponent" />
|
||||
</div>
|
||||
<div class="phone-tab-bar">
|
||||
<div v-for="tab in tabs" :key="tab.key"
|
||||
class="phone-tab-item" :class="{ active: activeTab === tab.key }"
|
||||
@click="switchTab(tab.key)">
|
||||
<span class="tab-icon"><component :is="tab.icon" style="width:18px;height:18px;" /></span>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, markRaw } from 'vue'
|
||||
import { HomeFilled, Camera, List, UserFilled } from '@element-plus/icons-vue'
|
||||
import MobileLogin from '../views/mobile/MobileLogin.vue'
|
||||
import ProjectSwitch from '../views/mobile/ProjectSwitch.vue'
|
||||
import MobileHome from '../views/mobile/MobileHome.vue'
|
||||
import ClockIn from '../views/mobile/ClockIn.vue'
|
||||
import PersonalRecords from '../views/mobile/PersonalRecords.vue'
|
||||
|
||||
const props = defineProps({ initialPage: { type: String, default: 'home' } })
|
||||
const emit = defineEmits(['tab-change'])
|
||||
|
||||
const activeTab = ref(props.initialPage)
|
||||
|
||||
const tabs = [
|
||||
{ key: 'home', label: '首页', icon: markRaw(HomeFilled) },
|
||||
{ key: 'clockin', label: '打卡', icon: markRaw(Camera) },
|
||||
{ key: 'records', label: '记录', icon: markRaw(List) },
|
||||
{ key: 'profile', label: '我的', icon: markRaw(UserFilled) },
|
||||
]
|
||||
|
||||
const componentMap = {
|
||||
'home': markRaw(MobileHome),
|
||||
'clockin': markRaw(ClockIn),
|
||||
'records': markRaw(PersonalRecords),
|
||||
'profile': markRaw(ProjectSwitch),
|
||||
}
|
||||
|
||||
const currentComponent = computed(() => componentMap[activeTab.value] || MobileHome)
|
||||
|
||||
function switchTab(key) {
|
||||
activeTab.value = key
|
||||
emit('tab-change', key)
|
||||
}
|
||||
</script>
|
||||
31
20260515/src/components/ScoreRing.vue
Normal file
31
20260515/src/components/ScoreRing.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div style="position:relative;display:inline-flex;align-items:center;justify-content:center;">
|
||||
<svg width="100" height="100" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="#f0e8df" stroke-width="8" />
|
||||
<circle cx="50" cy="50" r="42" fill="none" :stroke="color" stroke-width="8"
|
||||
stroke-linecap="round" :stroke-dasharray="circumference"
|
||||
:stroke-dashoffset="offset"
|
||||
transform="rotate(-90 50 50)"
|
||||
style="transition: stroke-dashoffset 0.6s ease;" />
|
||||
</svg>
|
||||
<div style="position:absolute;text-align:center;">
|
||||
<div style="font-size:28px;font-weight:700;" :style="{ color }">{{ score }}</div>
|
||||
<div style="font-size:11px;color:#999;">/ 10 分</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({ score: { type: Number, default: 0 } })
|
||||
const circumference = 2 * Math.PI * 42
|
||||
|
||||
const color = computed(() => {
|
||||
if (props.score >= 10) return '#67c23a'
|
||||
if (props.score >= 7) return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
})
|
||||
|
||||
const offset = computed(() => circumference * (1 - props.score / 10))
|
||||
</script>
|
||||
21
20260515/src/components/StatCard.vue
Normal file
21
20260515/src/components/StatCard.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="card-soft" style="display:flex;align-items:center;gap:16px;min-width:200px;">
|
||||
<div :style="{ width:'48px',height:'48px',borderRadius:'12px',background:bgColor,display:'flex',alignItems:'center',justifyContent:'center',fontSize:'22px',color:'#fff' }">
|
||||
<component :is="icon" />
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:13px;color:#999;">{{ label }}</div>
|
||||
<div style="font-size:24px;font-weight:700;color:#303133;">{{ value }}<span v-if="unit" style="font-size:14px;font-weight:400;color:#999;margin-left:2px;">{{ unit }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: { type: [String, Object, Function], default: null },
|
||||
label: String,
|
||||
value: [String, Number],
|
||||
unit: { type: String, default: '' },
|
||||
bgColor: { type: String, default: '#e67e22' }
|
||||
})
|
||||
</script>
|
||||
10
20260515/src/layouts/MobileFrame.vue
Normal file
10
20260515/src/layouts/MobileFrame.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div style="display:flex;justify-content:center;align-items:center;min-height:100vh;background:#f0e8df;">
|
||||
<PhoneSimulator :initialPage="page" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PhoneSimulator from '../components/PhoneSimulator.vue'
|
||||
defineProps({ page: { type: String, default: 'home' } })
|
||||
</script>
|
||||
63
20260515/src/layouts/PcLayout.vue
Normal file
63
20260515/src/layouts/PcLayout.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div style="display:flex;min-height:100vh;">
|
||||
<div class="sidebar" style="width:220px;flex-shrink:0;display:flex;flex-direction:column;">
|
||||
<div style="padding:20px;color:#fff;font-size:16px;font-weight:700;border-bottom:1px solid rgba(255,255,255,0.1);letter-spacing:1px;">
|
||||
建筑管理系统
|
||||
</div>
|
||||
<div style="flex:1;padding:8px 0;">
|
||||
<div v-for="item in menuItems" :key="item.path"
|
||||
@click="$router.push(item.path)"
|
||||
:style="{
|
||||
padding:'12px 24px', cursor:'pointer', fontSize:'14px',
|
||||
color: isActive(item.path) ? '#fff' : '#b0a89a',
|
||||
background: isActive(item.path) ? 'rgba(255,255,255,0.08)' : 'transparent',
|
||||
borderLeft: isActive(item.path) ? '3px solid #e67e22' : '3px solid transparent',
|
||||
transition:'all 0.2s'
|
||||
}">
|
||||
<el-icon style="margin-right:8px;"><component :is="item.icon" /></el-icon>{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;">
|
||||
<div style="height:56px;background:#fff;border-bottom:1px solid #f0e8df;display:flex;align-items:center;justify-content:flex-end;padding:0 24px;gap:16px;flex-shrink:0;">
|
||||
<el-button @click="mobileDrawer = true" size="small" round :icon="Iphone">手机预览</el-button>
|
||||
<span style="font-size:14px;color:#606266;display:flex;align-items:center;gap:6px;"><el-icon><UserFilled /></el-icon>管理员</span>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;padding:20px;overflow-y:auto;background:#fdf8f3;">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="mobileDrawer" title="小程序预览" size="420px" direction="rtl">
|
||||
<div style="display:flex;justify-content:center;">
|
||||
<PhoneSimulator :initialPage="mobileTab" @tab-change="mobileTab = $event" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, markRaw } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Monitor, FolderOpened, User, Calendar, Money, Setting, Iphone, UserFilled } from '@element-plus/icons-vue'
|
||||
import PhoneSimulator from '../components/PhoneSimulator.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const mobileDrawer = ref(false)
|
||||
const mobileTab = ref('home')
|
||||
|
||||
const menuItems = [
|
||||
{ path: '/pc/dashboard', label: '仪表盘', icon: markRaw(Monitor) },
|
||||
{ path: '/pc/projects', label: '项目管理', icon: markRaw(FolderOpened) },
|
||||
{ path: '/pc/workers', label: '人员管理', icon: markRaw(User) },
|
||||
{ path: '/pc/attendance', label: '考勤管理', icon: markRaw(Calendar) },
|
||||
{ path: '/pc/finance', label: '财务管理', icon: markRaw(Money) },
|
||||
{ path: '/pc/settings', label: '系统设置', icon: markRaw(Setting) },
|
||||
]
|
||||
|
||||
function isActive(path) {
|
||||
return route.path === path
|
||||
}
|
||||
</script>
|
||||
15
20260515/src/main.js
Normal file
15
20260515/src/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/theme.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
app.use(router)
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
app.mount('#app')
|
||||
47
20260515/src/router/index.js
Normal file
47
20260515/src/router/index.js
Normal file
@ -0,0 +1,47 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/pc/dashboard'
|
||||
},
|
||||
{
|
||||
path: '/pc/login',
|
||||
name: 'PcLogin',
|
||||
component: () => import('../views/pc/PcLogin.vue')
|
||||
},
|
||||
{
|
||||
path: '/pc',
|
||||
component: () => import('../layouts/PcLayout.vue'),
|
||||
children: [
|
||||
{ path: 'dashboard', name: 'Dashboard', component: () => import('../views/pc/Dashboard.vue') },
|
||||
{ path: 'projects', name: 'Projects', component: () => import('../views/pc/ProjectMgmt.vue') },
|
||||
{ path: 'workers', name: 'Workers', component: () => import('../views/pc/WorkerMgmt.vue') },
|
||||
{ path: 'attendance', name: 'Attendance', component: () => import('../views/pc/AttendanceMgmt.vue') },
|
||||
{ path: 'finance', name: 'Finance', component: () => import('../views/pc/FinanceMgmt.vue') },
|
||||
{ path: 'settings', name: 'Settings', component: () => import('../views/pc/SystemSettings.vue') },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/mobile/login',
|
||||
name: 'MobileLogin',
|
||||
component: () => import('../views/mobile/MobileLogin.vue')
|
||||
},
|
||||
{
|
||||
path: '/mobile/home',
|
||||
name: 'MobileHome',
|
||||
component: () => import('../layouts/MobileFrame.vue'),
|
||||
props: { page: 'home' }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/pc/dashboard'
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
155
20260515/src/stores/index.js
Normal file
155
20260515/src/stores/index.js
Normal file
@ -0,0 +1,155 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const now = new Date()
|
||||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
|
||||
const initialProjects = [
|
||||
{ id: 1, name: '长沙万科金域国际二期', location: '湖南省长沙市岳麓区', contractAmount: 1200000, prepaymentReceived: 360000, nextPaymentDate: '2026-06-15', nextPaymentAmount: 300000, outstandingBalance: 540000, status: '进行中' },
|
||||
{ id: 2, name: '南昌绿地国际博览城', location: '江西省南昌市红谷滩区', contractAmount: 980000, prepaymentReceived: 300000, nextPaymentDate: '2026-06-30', nextPaymentAmount: 250000, outstandingBalance: 430000, status: '进行中' },
|
||||
{ id: 3, name: '九江中梁首府', location: '江西省九江市濂溪区', contractAmount: 750000, prepaymentReceived: 750000, nextPaymentDate: '-', nextPaymentAmount: 0, outstandingBalance: 0, status: '已完工' },
|
||||
{ id: 4, name: '西宁碧桂园御湖', location: '青海省西宁市城西区', contractAmount: 1500000, prepaymentReceived: 500000, nextPaymentDate: '2026-05-20', nextPaymentAmount: 400000, outstandingBalance: 600000, status: '进行中' },
|
||||
{ id: 5, name: '衡阳美的梧桐庄园', location: '湖南省衡阳市蒸湘区', contractAmount: 860000, prepaymentReceived: 200000, nextPaymentDate: '2026-07-01', nextPaymentAmount: 300000, outstandingBalance: 360000, status: '筹备中' },
|
||||
]
|
||||
|
||||
const initialWorkers = [
|
||||
{ id: 1, name: '张建国', phone: '13812345601', assignedProjectId: 1, joinDate: '2025-03-15', status: '在岗' },
|
||||
{ id: 2, name: '李大军', phone: '13812345602', assignedProjectId: 1, joinDate: '2025-04-20', status: '在岗' },
|
||||
{ id: 3, name: '王永强', phone: '13812345603', assignedProjectId: 1, joinDate: '2025-05-10', status: '在岗' },
|
||||
{ id: 4, name: '刘志明', phone: '13812345604', assignedProjectId: 2, joinDate: '2025-06-01', status: '在岗' },
|
||||
{ id: 5, name: '陈海峰', phone: '13812345605', assignedProjectId: 2, joinDate: '2025-07-15', status: '在岗' },
|
||||
{ id: 6, name: '赵大勇', phone: '13812345606', assignedProjectId: 4, joinDate: '2025-08-01', status: '在岗' },
|
||||
{ id: 7, name: '周文斌', phone: '13812345607', assignedProjectId: 4, joinDate: '2025-09-10', status: '在岗' },
|
||||
{ id: 8, name: '吴国栋', phone: '13812345608', assignedProjectId: 4, joinDate: '2025-10-20', status: '休假' },
|
||||
{ id: 9, name: '郑小军', phone: '13812345609', assignedProjectId: 5, joinDate: '2026-01-05', status: '在岗' },
|
||||
{ id: 10, name: '马洪涛', phone: '13812345610', assignedProjectId: 5, joinDate: '2026-02-15', status: '在岗' },
|
||||
]
|
||||
|
||||
const initialAttendance = [
|
||||
{ id: 1, workerId: 1, projectId: 1, date: '2026-05-14', morningClockIn: '07:45', eveningClockOut: '18:05', score: 10, hoursWorked: 10, dailyWage: 100, status: '正常' },
|
||||
{ id: 2, workerId: 2, projectId: 1, date: '2026-05-14', morningClockIn: '07:50', eveningClockOut: '17:00', score: 9, hoursWorked: 9, dailyWage: 90, status: '正常' },
|
||||
{ id: 3, workerId: 3, projectId: 1, date: '2026-05-14', morningClockIn: '08:10', eveningClockOut: '18:00', score: 8, hoursWorked: 8, dailyWage: 80, status: '迟到' },
|
||||
{ id: 4, workerId: 1, projectId: 1, date: '2026-05-15', morningClockIn: '07:30', eveningClockOut: '18:10', score: 10, hoursWorked: 10, dailyWage: 100, status: '正常' },
|
||||
{ id: 5, workerId: 4, projectId: 2, date: '2026-05-14', morningClockIn: '07:55', eveningClockOut: '18:00', score: 10, hoursWorked: 10, dailyWage: 100, status: '正常' },
|
||||
{ id: 6, workerId: 5, projectId: 2, date: '2026-05-14', morningClockIn: '-', eveningClockOut: '-', score: 0, hoursWorked: 0, dailyWage: 0, status: '缺勤' },
|
||||
{ id: 7, workerId: 6, projectId: 4, date: '2026-05-14', morningClockIn: '07:40', eveningClockOut: '18:05', score: 10, hoursWorked: 10, dailyWage: 100, status: '正常' },
|
||||
]
|
||||
|
||||
const initialAdmins = [
|
||||
{ id: 1, username: 'admin', role: '超级管理员', lastLogin: '2026-05-15 08:30' },
|
||||
{ id: 2, username: 'zhangsan', role: '管理员', lastLogin: '2026-05-14 17:45' },
|
||||
]
|
||||
|
||||
const initialLogs = [
|
||||
{ id: 1, adminId: 1, action: '手动补卡', target: '李大军', detail: '2026-05-14 下班打卡补录 18:00', time: '2026-05-14 19:30' },
|
||||
{ id: 2, adminId: 1, action: '删除人员', target: '临时工-孙小伟', detail: '项目完工,人员清退', time: '2026-05-13 16:00' },
|
||||
{ id: 3, adminId: 2, action: '修改工时', target: '王永强', detail: '5月13日工时从8小时调整为10小时', time: '2026-05-14 09:15' },
|
||||
{ id: 4, adminId: 1, action: '新增项目', target: '衡阳美的梧桐庄园', detail: '合同金额 860,000 元', time: '2026-05-10 14:00' },
|
||||
]
|
||||
|
||||
let nextId = { projects: 6, workers: 11, attendance: 8, admins: 3, logs: 5 }
|
||||
|
||||
export const store = reactive({
|
||||
projects: [...initialProjects],
|
||||
workers: [...initialWorkers],
|
||||
attendance: [...initialAttendance],
|
||||
admins: [...initialAdmins],
|
||||
logs: [...initialLogs],
|
||||
|
||||
currentProjectId: 1,
|
||||
currentWorkerId: 1,
|
||||
gpsInRange: true,
|
||||
|
||||
addProject(project) {
|
||||
this.projects.push({ ...project, id: nextId.projects++ })
|
||||
this.addLog('新增项目', project.name, `合同金额 ${project.contractAmount.toLocaleString()} 元`)
|
||||
},
|
||||
updateProject(id, data) {
|
||||
const idx = this.projects.findIndex(p => p.id === id)
|
||||
if (idx !== -1) Object.assign(this.projects[idx], data)
|
||||
},
|
||||
deleteProject(id) {
|
||||
const p = this.projects.find(p => p.id === id)
|
||||
this.projects = this.projects.filter(p => p.id !== id)
|
||||
if (p) this.addLog('删除项目', p.name, '')
|
||||
},
|
||||
|
||||
addWorker(worker) {
|
||||
this.workers.push({ ...worker, id: nextId.workers++ })
|
||||
this.addLog('新增人员', worker.name, `分配到项目ID: ${worker.assignedProjectId}`)
|
||||
},
|
||||
deleteWorker(id) {
|
||||
const w = this.workers.find(w => w.id === id)
|
||||
this.workers = this.workers.filter(w => w.id !== id)
|
||||
if (w) this.addLog('删除人员', w.name, '人员清退')
|
||||
},
|
||||
assignProject(workerId, projectId) {
|
||||
const w = this.workers.find(w => w.id === workerId)
|
||||
if (w) w.assignedProjectId = projectId
|
||||
},
|
||||
|
||||
addAttendance(record) {
|
||||
this.attendance.push({ ...record, id: nextId.attendance++ })
|
||||
},
|
||||
updateAttendance(id, data) {
|
||||
const idx = this.attendance.findIndex(a => a.id === id)
|
||||
if (idx !== -1) {
|
||||
Object.assign(this.attendance[idx], data, { status: '手动调整' })
|
||||
this.addLog('修改工时', `记录#${id}`, `调整为 ${data.hoursWorked} 小时`)
|
||||
}
|
||||
},
|
||||
manualClockIn(workerId, projectId, date, morning, evening) {
|
||||
const hours = calculateHours(morning, evening)
|
||||
const score = calculateScore(morning, evening)
|
||||
this.attendance.push({
|
||||
id: nextId.attendance++, workerId, projectId, date,
|
||||
morningClockIn: morning, eveningClockOut: evening,
|
||||
score, hoursWorked: hours, dailyWage: score * 10,
|
||||
status: '手动调整'
|
||||
})
|
||||
const worker = this.workers.find(w => w.id === workerId)
|
||||
this.addLog('手动补卡', worker ? worker.name : `工人#${workerId}`, `${date} ${morning}-${evening}`)
|
||||
},
|
||||
|
||||
addLog(action, target, detail) {
|
||||
this.logs.unshift({
|
||||
id: nextId.logs++, adminId: 1, action, target, detail,
|
||||
time: new Date().toLocaleString('zh-CN', { hour12: false })
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function calculateHours(morning, evening) {
|
||||
if (morning === '-' || evening === '-') return 0
|
||||
const mparts = morning.split(':').map(Number)
|
||||
const eparts = evening.split(':').map(Number)
|
||||
return Math.round((eparts[0] - mparts[0] + (eparts[1] - mparts[1]) / 60) * 10) / 10
|
||||
}
|
||||
|
||||
function calculateScore(morning, evening) {
|
||||
const hours = calculateHours(morning, evening)
|
||||
if (hours >= 10) return 10
|
||||
return Math.max(0, Math.round(hours))
|
||||
}
|
||||
|
||||
export function getTodayAttendance(workerId) {
|
||||
return store.attendance.find(a => a.workerId === workerId && a.date === today)
|
||||
}
|
||||
|
||||
export function getWorkerAttendance(workerId) {
|
||||
return store.attendance.filter(a => a.workerId === workerId).sort((a, b) => b.date.localeCompare(a.date))
|
||||
}
|
||||
|
||||
export function getProjectWorkerCount(projectId) {
|
||||
return store.workers.filter(w => w.assignedProjectId === projectId).length
|
||||
}
|
||||
|
||||
export function getTodayStats() {
|
||||
const totalInPost = store.workers.filter(w => w.status === '在岗').length
|
||||
const todayRecords = store.attendance.filter(a => a.date === today)
|
||||
const present = todayRecords.filter(r => r.status !== '缺勤').length
|
||||
return {
|
||||
total: totalInPost,
|
||||
present,
|
||||
rate: totalInPost > 0 ? Math.round(present / totalInPost * 100) : 0
|
||||
}
|
||||
}
|
||||
108
20260515/src/styles/theme.css
Normal file
108
20260515/src/styles/theme.css
Normal file
@ -0,0 +1,108 @@
|
||||
:root {
|
||||
--el-color-primary: #e67e22;
|
||||
--el-color-primary-light-1: #e98e3f;
|
||||
--el-color-primary-light-2: #ed9e5c;
|
||||
--el-color-primary-light-3: #f0ae79;
|
||||
--el-color-primary-light-4: #f4be96;
|
||||
--el-color-primary-light-5: #f7ceb3;
|
||||
--el-color-primary-light-6: #fadecf;
|
||||
--el-color-primary-light-7: #fceeeb;
|
||||
--el-color-primary-light-8: #fef8f5;
|
||||
--el-color-primary-light-9: #fefcfa;
|
||||
--el-color-primary-dark-1: #cf711f;
|
||||
--el-color-success: #67c23a;
|
||||
--el-color-warning: #e6a23c;
|
||||
--el-color-danger: #f56c6c;
|
||||
--el-bg-color-page: #fdf8f3;
|
||||
--el-bg-color: #ffffff;
|
||||
--el-border-radius-base: 8px;
|
||||
--el-border-radius-small: 6px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: var(--el-bg-color-page);
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: linear-gradient(180deg, #3a3226 0%, #4a4033 100%);
|
||||
}
|
||||
|
||||
.card-soft {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.phone-simulator {
|
||||
width: 375px;
|
||||
height: 667px;
|
||||
border: 3px solid #333;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.phone-status-bar {
|
||||
height: 24px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.phone-header {
|
||||
height: 42px;
|
||||
background: linear-gradient(135deg, #e67e22, #f0a04b);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.phone-content {
|
||||
height: calc(667px - 24px - 42px - 50px);
|
||||
overflow-y: auto;
|
||||
background: #fdf8f3;
|
||||
}
|
||||
|
||||
.phone-tab-bar {
|
||||
height: 50px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0e8df;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.phone-tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: #b0a090;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.phone-tab-item.active {
|
||||
color: #e67e22;
|
||||
}
|
||||
|
||||
.phone-tab-item .tab-icon {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
.phone-tab-item .tab-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
67
20260515/src/views/mobile/ClockIn.vue
Normal file
67
20260515/src/views/mobile/ClockIn.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div style="padding:24px 16px;text-align:center;">
|
||||
<GpsBadge :inRange="store.gpsInRange" style="margin-bottom:24px;" />
|
||||
|
||||
<div style="font-size:13px;color:#999;margin-bottom:8px;">今日考勤</div>
|
||||
|
||||
<div style="margin:20px 0;">
|
||||
<div @click="doClockIn"
|
||||
:style="{
|
||||
width:'120px',height:'120px',borderRadius:'50%',display:'inline-flex',
|
||||
alignItems:'center',justifyContent:'center',cursor:'pointer',
|
||||
background: clockedIn ? '#f0e8df' : 'linear-gradient(135deg, #e67e22, #f0a04b)',
|
||||
color: clockedIn ? '#999' : '#fff', fontSize:'18px', fontWeight:700,
|
||||
transition:'all 0.3s', boxShadow: clockedIn ? 'none' : '0 4px 20px rgba(230,126,34,0.4)'
|
||||
}">
|
||||
{{ clockedIn ? '已打卡' : '打卡' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:12px;color:#999;">
|
||||
<div v-if="todayRecord">
|
||||
<div>上班: {{ todayRecord.morningClockIn }} | 下班: {{ todayRecord.eveningClockOut }}</div>
|
||||
<div style="margin-top:4px;font-weight:600;" :style="{ color: todayRecord.score >= 10 ? '#67c23a' : '#e6a23c' }">
|
||||
评分 {{ todayRecord.score }} 分 - 日薪 {{ todayRecord.dailyWage }} 元
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>尚未打卡</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:32px;font-size:12px;color:#999;">
|
||||
<div style="margin-bottom:6px;">演示: GPS 范围切换</div>
|
||||
<el-switch v-model="store.gpsInRange" active-text="范围内" inactive-text="超出" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { store, getTodayAttendance } from '../../stores/index.js'
|
||||
import GpsBadge from '../../components/GpsBadge.vue'
|
||||
|
||||
const todayRecord = computed(() => getTodayAttendance(store.currentWorkerId))
|
||||
const clockedIn = computed(() => !!todayRecord.value && todayRecord.value.morningClockIn !== '-')
|
||||
|
||||
function doClockIn() {
|
||||
if (!store.gpsInRange) return
|
||||
const now = new Date()
|
||||
const time = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`
|
||||
const today = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`
|
||||
|
||||
if (!todayRecord.value) {
|
||||
store.attendance.push({
|
||||
id: Date.now(), workerId: store.currentWorkerId, projectId: store.currentProjectId,
|
||||
date: today, morningClockIn: time, eveningClockOut: '-', score: 0, hoursWorked: 0, dailyWage: 0, status: '正常'
|
||||
})
|
||||
} else if (todayRecord.value.eveningClockOut === '-') {
|
||||
const mparts = todayRecord.value.morningClockIn.split(':').map(Number)
|
||||
const eparts = time.split(':').map(Number)
|
||||
const hours = Math.max(0, eparts[0] - mparts[0] + (eparts[1] - mparts[1]) / 60)
|
||||
const score = Math.min(10, Math.max(0, Math.round(hours)))
|
||||
Object.assign(todayRecord.value, {
|
||||
eveningClockOut: time, hoursWorked: Math.round(hours * 10) / 10,
|
||||
score, dailyWage: score * 10
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
37
20260515/src/views/mobile/MobileHome.vue
Normal file
37
20260515/src/views/mobile/MobileHome.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div style="padding:16px;">
|
||||
<div style="background:#fff;border-radius:12px;padding:16px;margin-bottom:12px;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
|
||||
<div style="font-size:14px;font-weight:600;color:#303133;">{{ currentProject?.name || '-' }}</div>
|
||||
<div style="font-size:11px;color:#999;margin-top:4px;">{{ currentProject?.location || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div style="background:#fff;border-radius:12px;padding:20px;margin-bottom:12px;box-shadow:0 1px 6px rgba(0,0,0,0.04);text-align:center;">
|
||||
<div style="font-size:13px;color:#999;margin-bottom:12px;">今日考勤评分</div>
|
||||
<ScoreRing :score="todayRecord?.score || 0" />
|
||||
<div style="margin-top:12px;">
|
||||
<div style="font-size:12px;color:#666;">上班: {{ todayRecord?.morningClockIn || '-' }} | 下班: {{ todayRecord?.eveningClockOut || '-' }}</div>
|
||||
<div style="font-size:12px;color:#e67e22;font-weight:600;margin-top:4px;">预计日薪: {{ (todayRecord?.dailyWage || 0) }} 元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px;">
|
||||
<div style="flex:1;background:#fff;border-radius:10px;padding:14px;text-align:center;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
|
||||
<div style="font-size:20px;font-weight:700;color:#e67e22;">{{ store.workers.filter(w=>w.status==='在岗').length }}</div>
|
||||
<div style="font-size:11px;color:#999;">在岗工人</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#fff;border-radius:10px;padding:14px;text-align:center;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
|
||||
<div style="font-size:20px;font-weight:700;color:#67c23a;">{{ store.projects.filter(p=>p.status==='进行中').length }}</div>
|
||||
<div style="font-size:11px;color:#999;">进行中项目</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { store, getTodayAttendance } from '../../stores/index.js'
|
||||
import ScoreRing from '../../components/ScoreRing.vue'
|
||||
|
||||
const currentProject = computed(() => store.projects.find(p => p.id === store.currentProjectId))
|
||||
const todayRecord = computed(() => getTodayAttendance(store.currentWorkerId))
|
||||
</script>
|
||||
32
20260515/src/views/mobile/MobileLogin.vue
Normal file
32
20260515/src/views/mobile/MobileLogin.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div style="padding:40px 24px;text-align:center;">
|
||||
<div style="font-size:20px;font-weight:700;color:#303133;margin-bottom:8px;">建筑管理系统</div>
|
||||
<div style="font-size:12px;color:#999;margin-bottom:32px;">微信小程序端</div>
|
||||
<div style="font-size:13px;color:#666;margin-bottom:16px;">手机号登录</div>
|
||||
<input class="mobile-input" v-model="phone" placeholder="请输入手机号" />
|
||||
<input class="mobile-input" v-model="code" placeholder="验证码" style="margin-bottom:24px;" />
|
||||
<button class="mobile-btn" @click="doLogin">登 录</button>
|
||||
<div style="font-size:11px;color:#ccc;margin-top:20px;">首次登录自动注册</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
function doLogin() { /* 原型演示,直接切换 */ }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-input {
|
||||
width: 100%; padding: 10px 12px; border: 1px solid #e0d6cc;
|
||||
border-radius: 8px; font-size: 14px; margin-bottom: 12px;
|
||||
background: #fff; box-sizing: border-box; outline: none;
|
||||
}
|
||||
.mobile-input:focus { border-color: #e67e22; }
|
||||
.mobile-btn {
|
||||
width: 100%; padding: 12px; background: linear-gradient(135deg, #e67e22, #f0a04b);
|
||||
border: none; border-radius: 24px; color: #fff; font-size: 15px;
|
||||
font-weight: 600; cursor: pointer; letter-spacing: 2px;
|
||||
}
|
||||
</style>
|
||||
51
20260515/src/views/mobile/PersonalRecords.vue
Normal file
51
20260515/src/views/mobile/PersonalRecords.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div style="padding:16px;">
|
||||
<div style="background:#fff;border-radius:12px;padding:16px;margin-bottom:12px;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
|
||||
<div style="display:flex;justify-content:space-around;text-align:center;">
|
||||
<div>
|
||||
<div style="font-size:22px;font-weight:700;color:#e67e22;">{{ monthStats.totalDays }}</div>
|
||||
<div style="font-size:11px;color:#999;">出勤天数</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:22px;font-weight:700;color:#303133;">{{ monthStats.totalHours }}</div>
|
||||
<div style="font-size:11px;color:#999;">总工时</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:22px;font-weight:700;color:#67c23a;">{{ monthStats.totalWage }}</div>
|
||||
<div style="font-size:11px;color:#999;">月薪合计</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:14px;font-weight:600;color:#303133;margin-bottom:10px;">考勤明细</div>
|
||||
<div v-for="r in workerRecords" :key="r.id"
|
||||
style="background:#fff;border-radius:10px;padding:14px;margin-bottom:8px;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<div style="font-size:13px;color:#303133;">{{ r.date }}</div>
|
||||
<div style="font-size:11px;color:#999;margin-top:2px;">{{ r.morningClockIn }} - {{ r.eveningClockOut }}</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div :style="{ fontSize:'16px',fontWeight:700, color: r.score>=10?'#67c23a':r.score>=7?'#e6a23c':'#f56c6c' }">{{ r.score }} 分</div>
|
||||
<div style="font-size:11px;color:#999;">{{ r.dailyWage }} 元</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { store, getWorkerAttendance } from '../../stores/index.js'
|
||||
|
||||
const workerRecords = computed(() => getWorkerAttendance(store.currentWorkerId))
|
||||
|
||||
const monthStats = computed(() => {
|
||||
const records = workerRecords.value
|
||||
return {
|
||||
totalDays: records.filter(r => r.status !== '缺勤').length,
|
||||
totalHours: records.reduce((s, r) => s + r.hoursWorked, 0),
|
||||
totalWage: records.reduce((s, r) => s + r.dailyWage, 0)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
19
20260515/src/views/mobile/ProjectSwitch.vue
Normal file
19
20260515/src/views/mobile/ProjectSwitch.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div style="padding:20px 16px;">
|
||||
<div style="font-size:14px;font-weight:600;color:#303133;margin-bottom:16px;">切换项目</div>
|
||||
<div v-for="p in store.projects" :key="p.id"
|
||||
@click="store.currentProjectId = p.id"
|
||||
:style="{
|
||||
padding:'14px', borderRadius:'10px', marginBottom:'8px', cursor:'pointer',
|
||||
background: store.currentProjectId === p.id ? '#fef5ed' : '#fff',
|
||||
border: store.currentProjectId === p.id ? '2px solid #e67e22' : '1px solid #f0e8df'
|
||||
}">
|
||||
<div style="font-size:14px;font-weight:600;color:#303133;">{{ p.name }}</div>
|
||||
<div style="font-size:11px;color:#999;margin-top:4px;">{{ p.location }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { store } from '../../stores/index.js'
|
||||
</script>
|
||||
145
20260515/src/views/pc/AttendanceMgmt.vue
Normal file
145
20260515/src/views/pc/AttendanceMgmt.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
||||
<h2 style="margin:0;font-size:20px;">考勤管理</h2>
|
||||
<el-button type="primary" :icon="Clock" @click="openPatchDialog" round>手动补卡</el-button>
|
||||
</div>
|
||||
|
||||
<div class="card-soft" style="margin-bottom:16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
|
||||
<el-select v-model="filterProject" placeholder="按项目筛选" clearable style="width:200px;" size="small">
|
||||
<el-option v-for="p in store.projects" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="filterDate" type="date" placeholder="按日期筛选" size="small" value-format="YYYY-MM-DD" />
|
||||
<el-button size="small" @click="filterProject=null;filterDate=null;">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="card-soft">
|
||||
<el-table :data="filteredAttendance" stripe style="width:100%;" max-height="450">
|
||||
<el-table-column label="工人" width="100">
|
||||
<template #default="{ row }">{{ getWorkerName(row.workerId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目" min-width="160">
|
||||
<template #default="{ row }">{{ getProjectName(row.projectId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="date" label="日期" width="110" />
|
||||
<el-table-column prop="morningClockIn" label="上班打卡" width="90" />
|
||||
<el-table-column prop="eveningClockOut" label="下班打卡" width="90" />
|
||||
<el-table-column label="评分" width="80">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.score>=10?'#67c23a':row.score>=7?'#e6a23c':'#f56c6c', fontWeight:600 }">{{ row.score }} 分</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hoursWorked" label="工时" width="70" />
|
||||
<el-table-column prop="dailyWage" label="日薪" width="80">
|
||||
<template #default="{ row }">{{ row.dailyWage }} 元</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status==='正常'" type="success" size="small">正常</el-tag>
|
||||
<el-tag v-else-if="row.status==='手动调整'" type="warning" size="small">手动调整</el-tag>
|
||||
<el-tag v-else-if="row.status==='迟到'" type="info" size="small">迟到</el-tag>
|
||||
<el-tag v-else type="danger" size="small">缺勤</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" :icon="Edit" @click="openEditDialog(row)" link type="primary">调整</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="patchVisible" title="手动补卡" width="480px">
|
||||
<el-form :model="patchForm" label-width="90px">
|
||||
<el-form-item label="工人">
|
||||
<el-select v-model="patchForm.workerId" style="width:100%;">
|
||||
<el-option v-for="w in store.workers" :key="w.id" :label="w.name" :value="w.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目">
|
||||
<el-select v-model="patchForm.projectId" style="width:100%;">
|
||||
<el-option v-for="p in store.projects" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期"><el-input v-model="patchForm.date" placeholder="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="上班时间"><el-input v-model="patchForm.morning" placeholder="HH:MM 或 -" /></el-form-item>
|
||||
<el-form-item label="下班时间"><el-input v-model="patchForm.evening" placeholder="HH:MM 或 -" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="patchVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="doPatchClockIn">确认补卡</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="editVisible" title="调整工时" width="420px">
|
||||
<el-form :model="editForm" label-width="90px">
|
||||
<el-form-item label="上班打卡"><el-input v-model="editForm.morningClockIn" /></el-form-item>
|
||||
<el-form-item label="下班打卡"><el-input v-model="editForm.eveningClockOut" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="doUpdateAttendance">保存调整</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Clock, Edit } from '@element-plus/icons-vue'
|
||||
import { store } from '../../stores/index.js'
|
||||
|
||||
const filterProject = ref(null)
|
||||
const filterDate = ref(null)
|
||||
|
||||
const filteredAttendance = computed(() => {
|
||||
let list = [...store.attendance].sort((a, b) => b.date.localeCompare(a.date))
|
||||
if (filterProject.value) list = list.filter(a => a.projectId === filterProject.value)
|
||||
if (filterDate.value) {
|
||||
const ds = typeof filterDate.value === 'string' ? filterDate.value : filterDate.value.toISOString().slice(0, 10)
|
||||
list = list.filter(a => a.date === ds)
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
function getWorkerName(id) { const w = store.workers.find(w => w.id === id); return w ? w.name : `#${id}` }
|
||||
function getProjectName(id) { const p = store.projects.find(p => p.id === id); return p ? p.name : `#${id}` }
|
||||
|
||||
const patchVisible = ref(false)
|
||||
const patchForm = reactive({ workerId: 1, projectId: 1, date: '', morning: '', evening: '' })
|
||||
function openPatchDialog() {
|
||||
patchForm.date = new Date().toISOString().slice(0, 10)
|
||||
patchForm.morning = '08:00'
|
||||
patchForm.evening = '18:00'
|
||||
patchVisible.value = true
|
||||
}
|
||||
function doPatchClockIn() {
|
||||
store.manualClockIn(patchForm.workerId, patchForm.projectId, patchForm.date, patchForm.morning, patchForm.evening)
|
||||
patchVisible.value = false
|
||||
}
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editForm = reactive({ id: null, morningClockIn: '', eveningClockOut: '' })
|
||||
function openEditDialog(row) {
|
||||
editForm.id = row.id
|
||||
editForm.morningClockIn = row.morningClockIn
|
||||
editForm.eveningClockOut = row.eveningClockOut
|
||||
editVisible.value = true
|
||||
}
|
||||
function doUpdateAttendance() {
|
||||
const h = (() => {
|
||||
if (editForm.morningClockIn === '-' || editForm.eveningClockOut === '-') return 0
|
||||
const m = editForm.morningClockIn.split(':').map(Number)
|
||||
const e = editForm.eveningClockOut.split(':').map(Number)
|
||||
return Math.max(0, e[0] - m[0] + (e[1] - m[1]) / 60)
|
||||
})()
|
||||
store.updateAttendance(editForm.id, {
|
||||
morningClockIn: editForm.morningClockIn,
|
||||
eveningClockOut: editForm.eveningClockOut,
|
||||
hoursWorked: Math.round(h * 10) / 10,
|
||||
score: Math.min(10, Math.max(0, Math.round(h))),
|
||||
dailyWage: Math.min(10, Math.max(0, Math.round(h))) * 10
|
||||
})
|
||||
editVisible.value = false
|
||||
}
|
||||
</script>
|
||||
54
20260515/src/views/pc/Dashboard.vue
Normal file
54
20260515/src/views/pc/Dashboard.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 style="margin:0 0 20px;font-size:20px;">仪表盘</h2>
|
||||
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:20px;">
|
||||
<StatCard :icon="OfficeBuilding" label="项目总数" :value="store.projects.length" unit="个" bgColor="#e67e22" />
|
||||
<StatCard :icon="User" label="在岗工人" :value="store.workers.filter(w=>w.status==='在岗').length" unit="人" bgColor="#67c23a" />
|
||||
<StatCard :icon="TrendCharts" label="今日出勤率" :value="todayStats.rate" unit="%" bgColor="#409eff" />
|
||||
<StatCard :icon="Coin" label="待收总额" :value="(store.projects.reduce((s,p)=>s+p.outstandingBalance,0)/10000).toFixed(0)" unit="万元" bgColor="#e6a23c" />
|
||||
</div>
|
||||
|
||||
<div class="card-soft" style="margin-bottom:20px;">
|
||||
<h3 style="margin:0 0 16px;font-size:16px;">项目概览</h3>
|
||||
<el-table :data="store.projects" stripe style="width:100%;">
|
||||
<el-table-column prop="name" label="项目名称" min-width="180" />
|
||||
<el-table-column prop="location" label="位置" min-width="160" />
|
||||
<el-table-column prop="contractAmount" label="合同金额" width="120">
|
||||
<template #default="{ row }">{{ row.contractAmount.toLocaleString() }} 元</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="outstandingBalance" label="待收余额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.outstandingBalance > 0 ? '#e6a23c' : '#67c23a' }">
|
||||
{{ row.outstandingBalance.toLocaleString() }} 元
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status==='进行中'?'warning':row.status==='已完工'?'success':'info'" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="card-soft">
|
||||
<h3 style="margin:0 0 16px;font-size:16px;">近期操作</h3>
|
||||
<el-table :data="store.logs.slice(0, 5)" stripe style="width:100%;" size="small">
|
||||
<el-table-column prop="time" label="时间" width="160" />
|
||||
<el-table-column prop="action" label="操作" width="100" />
|
||||
<el-table-column prop="target" label="对象" width="120" />
|
||||
<el-table-column prop="detail" label="详情" min-width="200" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { OfficeBuilding, User, TrendCharts, Coin } from '@element-plus/icons-vue'
|
||||
import { store, getTodayStats } from '../../stores/index.js'
|
||||
import StatCard from '../../components/StatCard.vue'
|
||||
|
||||
const todayStats = computed(() => getTodayStats())
|
||||
</script>
|
||||
65
20260515/src/views/pc/FinanceMgmt.vue
Normal file
65
20260515/src/views/pc/FinanceMgmt.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 style="margin:0 0 20px;font-size:20px;">财务管理</h2>
|
||||
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:20px;">
|
||||
<div class="card-soft" style="flex:1;min-width:160px;text-align:center;">
|
||||
<div style="margin-bottom:6px;color:#e67e22;"><el-icon :size="22"><Money /></el-icon></div>
|
||||
<div style="font-size:13px;color:#999;">合同总金额</div>
|
||||
<div style="font-size:22px;font-weight:700;color:#303133;margin-top:4px;">{{ totalContract.toLocaleString() }}</div>
|
||||
<div style="font-size:12px;color:#999;">元</div>
|
||||
</div>
|
||||
<div class="card-soft" style="flex:1;min-width:160px;text-align:center;">
|
||||
<div style="margin-bottom:6px;color:#67c23a;"><el-icon :size="22"><CircleCheckFilled /></el-icon></div>
|
||||
<div style="font-size:13px;color:#999;">已收预付款</div>
|
||||
<div style="font-size:22px;font-weight:700;color:#67c23a;margin-top:4px;">{{ totalPrepayment.toLocaleString() }}</div>
|
||||
<div style="font-size:12px;color:#999;">元</div>
|
||||
</div>
|
||||
<div class="card-soft" style="flex:1;min-width:160px;text-align:center;">
|
||||
<div style="margin-bottom:6px;color:#e6a23c;"><el-icon :size="22"><WarningFilled /></el-icon></div>
|
||||
<div style="font-size:13px;color:#999;">待收余额</div>
|
||||
<div style="font-size:22px;font-weight:700;color:#e6a23c;margin-top:4px;">{{ totalOutstanding.toLocaleString() }}</div>
|
||||
<div style="font-size:12px;color:#999;">元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-soft">
|
||||
<h3 style="margin:0 0 16px;font-size:16px;">回款追踪明细</h3>
|
||||
<el-table :data="store.projects" stripe style="width:100%;">
|
||||
<el-table-column prop="name" label="项目名称" min-width="160" />
|
||||
<el-table-column prop="contractAmount" label="合同金额" width="120">
|
||||
<template #default="{ row }">{{ row.contractAmount.toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="prepaymentReceived" label="预付款已收" width="120">
|
||||
<template #default="{ row }"><span style="color:#67c23a;">{{ row.prepaymentReceived.toLocaleString() }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nextPaymentDate" label="下笔款项日期" width="130" />
|
||||
<el-table-column prop="nextPaymentAmount" label="下笔款项金额" width="130">
|
||||
<template #default="{ row }">{{ row.nextPaymentAmount ? row.nextPaymentAmount.toLocaleString() : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="待收余额" width="130">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.outstandingBalance > 0 ? '#e6a23c' : '#67c23a', fontWeight: 600 }">{{ row.outstandingBalance.toLocaleString() }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回款状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.outstandingBalance === 0" type="success" size="small">已结清</el-tag>
|
||||
<el-tag v-else-if="row.prepaymentReceived > 0" type="warning" size="small">部分回款</el-tag>
|
||||
<el-tag v-else type="info" size="small">未回款</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Money, CircleCheckFilled, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { store } from '../../stores/index.js'
|
||||
|
||||
const totalContract = computed(() => store.projects.reduce((s, p) => s + p.contractAmount, 0))
|
||||
const totalPrepayment = computed(() => store.projects.reduce((s, p) => s + p.prepaymentReceived, 0))
|
||||
const totalOutstanding = computed(() => store.projects.reduce((s, p) => s + p.outstandingBalance, 0))
|
||||
</script>
|
||||
38
20260515/src/views/pc/PcLogin.vue
Normal file
38
20260515/src/views/pc/PcLogin.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div style="display:flex;justify-content:center;align-items:center;min-height:100vh;background:linear-gradient(135deg, #fdf8f3 0%, #f0e8df 100%);">
|
||||
<div style="width:400px;background:#fff;border-radius:16px;padding:48px 40px;box-shadow:0 8px 40px rgba(0,0,0,0.08);">
|
||||
<div style="text-align:center;margin-bottom:32px;">
|
||||
<div style="font-size:24px;font-weight:700;color:#303133;">建筑数字化管理系统</div>
|
||||
<div style="font-size:13px;color:#999;margin-top:8px;">Construction Digital Management</div>
|
||||
</div>
|
||||
<el-form @submit.prevent>
|
||||
<el-form-item>
|
||||
<el-input placeholder="账号" prefix-icon="User" v-model="username" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input placeholder="密码" prefix-icon="Lock" v-model="password" type="password" size="large" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="large" style="width:100%;" @click="login" round :icon="SwitchButton">登 录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div style="text-align:center;font-size:12px;color:#ccc;">管理员账号: admin / 密码任意</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { SwitchButton } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
|
||||
function login() {
|
||||
if (username.value && password.value) {
|
||||
router.push('/pc/dashboard')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
101
20260515/src/views/pc/ProjectMgmt.vue
Normal file
101
20260515/src/views/pc/ProjectMgmt.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
||||
<h2 style="margin:0;font-size:20px;">项目管理</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="openDialog(null)" round>新增项目</el-button>
|
||||
</div>
|
||||
|
||||
<div class="card-soft">
|
||||
<el-table :data="store.projects" stripe style="width:100%;">
|
||||
<el-table-column prop="name" label="项目名称" min-width="180" />
|
||||
<el-table-column prop="location" label="地理位置" min-width="160" />
|
||||
<el-table-column prop="contractAmount" label="合同金额" width="120">
|
||||
<template #default="{ row }">{{ row.contractAmount.toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="prepaymentReceived" label="预付款已收" width="110">
|
||||
<template #default="{ row }">{{ row.prepaymentReceived.toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nextPaymentDate" label="下笔款项日期" width="120" />
|
||||
<el-table-column prop="outstandingBalance" label="待收余额" width="110">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.outstandingBalance > 0 ? '#e6a23c' : '#67c23a', fontWeight:600 }">{{ row.outstandingBalance.toLocaleString() }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status==='进行中'?'warning':row.status==='已完工'?'success':'info'" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" :icon="Edit" @click="openDialog(row)" link type="primary">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该项目?" @confirm="store.deleteProject(row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" :icon="Delete" link type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑项目' : '新增项目'" width="560px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="项目名称"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="地理位置"><el-input v-model="form.location" /></el-form-item>
|
||||
<el-form-item label="合同金额"><el-input-number v-model="form.contractAmount" :min="0" :step="10000" style="width:100%;" /></el-form-item>
|
||||
<el-form-item label="预付款已收"><el-input-number v-model="form.prepaymentReceived" :min="0" :step="10000" style="width:100%;" /></el-form-item>
|
||||
<el-form-item label="下笔款项日期"><el-input v-model="form.nextPaymentDate" placeholder="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="下笔款项金额"><el-input-number v-model="form.nextPaymentAmount" :min="0" :step="10000" style="width:100%;" /></el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:100%;">
|
||||
<el-option label="筹备中" value="筹备中" />
|
||||
<el-option label="进行中" value="进行中" />
|
||||
<el-option label="已完工" value="已完工" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { store } from '../../stores/index.js'
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref(null)
|
||||
const form = reactive({
|
||||
name: '', location: '', contractAmount: 0, prepaymentReceived: 0,
|
||||
nextPaymentDate: '', nextPaymentAmount: 0, outstandingBalance: 0, status: '进行中'
|
||||
})
|
||||
|
||||
function openDialog(row) {
|
||||
if (row) {
|
||||
editingId.value = row.id
|
||||
Object.assign(form, { ...row })
|
||||
} else {
|
||||
editingId.value = null
|
||||
Object.assign(form, {
|
||||
name: '', location: '', contractAmount: 0, prepaymentReceived: 0,
|
||||
nextPaymentDate: '', nextPaymentAmount: 0, outstandingBalance: 0, status: '进行中'
|
||||
})
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function submit() {
|
||||
form.outstandingBalance = form.contractAmount - form.prepaymentReceived
|
||||
if (editingId.value) {
|
||||
store.updateProject(editingId.value, { ...form })
|
||||
} else {
|
||||
store.addProject({ ...form })
|
||||
}
|
||||
dialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
56
20260515/src/views/pc/SystemSettings.vue
Normal file
56
20260515/src/views/pc/SystemSettings.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 style="margin:0 0 20px;font-size:20px;">系统设置</h2>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="管理员账号" name="admins">
|
||||
<div class="card-soft">
|
||||
<el-table :data="store.admins" stripe style="width:100%;">
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="role" label="角色" width="150" />
|
||||
<el-table-column prop="lastLogin" label="最后登录" width="200" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="操作日志" name="logs">
|
||||
<div class="card-soft">
|
||||
<el-table :data="store.logs" stripe style="width:100%;" max-height="500">
|
||||
<el-table-column prop="time" label="时间" width="170" />
|
||||
<el-table-column prop="action" label="操作类型" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.action.includes('删除')" type="danger" size="small">{{ row.action }}</el-tag>
|
||||
<el-tag v-else-if="row.action.includes('新增')" type="success" size="small">{{ row.action }}</el-tag>
|
||||
<el-tag v-else type="warning" size="small">{{ row.action }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target" label="操作对象" width="140" />
|
||||
<el-table-column prop="detail" label="详情" min-width="250" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="系统参数" name="params">
|
||||
<div class="card-soft">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="系统版本">v1.0.0 原型</el-descriptions-item>
|
||||
<el-descriptions-item label="每日标准工时">10 小时</el-descriptions-item>
|
||||
<el-descriptions-item label="满分标准">10 分</el-descriptions-item>
|
||||
<el-descriptions-item label="日薪基准">100 元/天</el-descriptions-item>
|
||||
<el-descriptions-item label="打卡有效范围">项目位置 500 米内</el-descriptions-item>
|
||||
<el-descriptions-item label="上班打卡截止">08:00</el-descriptions-item>
|
||||
<el-descriptions-item label="下班打卡时间">18:00 左右</el-descriptions-item>
|
||||
<el-descriptions-item label="服务器">阿里云 ECS</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { store } from '../../stores/index.js'
|
||||
|
||||
const activeTab = ref('admins')
|
||||
</script>
|
||||
93
20260515/src/views/pc/WorkerMgmt.vue
Normal file
93
20260515/src/views/pc/WorkerMgmt.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
||||
<h2 style="margin:0;font-size:20px;">人员管理</h2>
|
||||
<el-button type="primary" :icon="Plus" @click="openDialog(null)" round>新增人员</el-button>
|
||||
</div>
|
||||
|
||||
<div class="card-soft">
|
||||
<el-table :data="store.workers" stripe style="width:100%;">
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column label="所属项目" min-width="160">
|
||||
<template #default="{ row }">{{ getProjectName(row.assignedProjectId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="joinDate" label="入场日期" width="110" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status==='在岗'?'success':'info'" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" :icon="Edit" @click="openDialog(row)" link type="primary">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该人员?" @confirm="store.deleteWorker(row.id)">
|
||||
<template #reference>
|
||||
<el-button size="small" :icon="Delete" link type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑人员' : '新增人员'" width="480px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item>
|
||||
<el-form-item label="所属项目">
|
||||
<el-select v-model="form.assignedProjectId" style="width:100%;">
|
||||
<el-option v-for="p in store.projects" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:100%;">
|
||||
<el-option label="在岗" value="在岗" />
|
||||
<el-option label="休假" value="休假" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { store } from '../../stores/index.js'
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref(null)
|
||||
const form = reactive({ name: '', phone: '', assignedProjectId: 1, joinDate: '', status: '在岗' })
|
||||
|
||||
function getProjectName(id) {
|
||||
const p = store.projects.find(p => p.id === id)
|
||||
return p ? p.name : '-'
|
||||
}
|
||||
|
||||
function openDialog(row) {
|
||||
if (row) {
|
||||
editingId.value = row.id
|
||||
Object.assign(form, { ...row })
|
||||
} else {
|
||||
editingId.value = null
|
||||
Object.assign(form, { name: '', phone: '', assignedProjectId: store.projects[0]?.id || 1, joinDate: new Date().toISOString().slice(0, 10), status: '在岗' })
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (editingId.value) {
|
||||
store.assignProject(editingId.value, form.assignedProjectId)
|
||||
const w = store.workers.find(w => w.id === editingId.value)
|
||||
if (w) { w.name = form.name; w.phone = form.phone; w.status = form.status }
|
||||
} else {
|
||||
store.addWorker({ ...form, joinDate: new Date().toISOString().slice(0, 10) })
|
||||
}
|
||||
dialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
10
20260515/vite.config.js
Normal file
10
20260515/vite.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user