feat(crm): 合同含税总金额自动填入订单总金额 & 移除冗余页面

- feat(crm/contract): 含税总额变化后自动填写订单总金额(可配置开关)
- fix(crm/receive): 修复金额单位错误(万元→元);清理未使用导入
- fix(contract/product): 产品备注设置默认值
- chore: 移除已废弃的 OrderDashboard 组件和 finance/order 页面
- feat(wms/hrm): 新增考勤异常管理页面(attendanceAbnormal.vue)
- chore: 移除 trae git 提交规则配置
This commit is contained in:
2026-06-06 13:01:38 +08:00
parent 724c1dd16f
commit 050dd1a965
7 changed files with 901 additions and 381 deletions

View File

@@ -1,9 +0,0 @@
---
alwaysApply: true
scene: git_message
---
使用中文编写提交信息, 提交信息格式为:
```
<type>(<scope>): <subject>
```

View File

@@ -1,163 +0,0 @@
<template>
<div
class="order-analysis-dashboard"
v-loading="loading"
>
<!-- 业绩区 -->
<el-tabs v-model="activeTab">
<el-tab-pane label="业绩总览" name="performance">
<PerformanceArea mode="mini" :performance-area="dashboardData.performanceArea" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import PerformanceArea from '@/views/wms/order/components/PerformanceArea.vue'
import CurrentSituationArea from '@/views/wms/order/components/CurrentSituationArea.vue'
import RecommendationArea from '@/views/wms/order/components/RecommendationArea.vue'
import { getDashboardData } from '@/api/wms/order'
export default {
name: 'OrderAnalysisDashboard',
components: {
PerformanceArea,
CurrentSituationArea,
RecommendationArea,
},
data() {
return {
// 新的数据结构
dashboardData: {
performanceArea: {},
currentSituationArea: {},
recommendationArea: {}
},
// 新增定时刷新相关数据
drawerVisible: false,
autoRefresh: false,
refreshInterval: 30, // 默认30秒
refreshTimer: null,
loading: false,
activeTab: 'performance'
}
},
created() {
this.fetchAllData()
this.loadRefreshSetting()
this.startAutoRefresh()
},
beforeDestroy() {
this.clearAutoRefresh()
},
methods: {
async fetchAllData() {
this.loading = true
try {
const res = await getDashboardData()
const data = res
// 更新新的数据结构
this.dashboardData = {
performanceArea: data.performanceArea || {},
currentSituationArea: data.currentSituationArea || {},
recommendationArea: data.recommendationArea || {}
}
} catch (error) {
console.error('获取数据看板数据失败:', error)
this.$message.error('获取数据失败,请稍后重试')
} finally {
this.loading = false
}
},
handleRefresh() {
this.fetchAllData()
},
// 定时刷新相关
startAutoRefresh() {
this.clearAutoRefresh()
if (this.autoRefresh) {
this.refreshTimer = setInterval(() => {
this.fetchAllData()
}, this.refreshInterval * 1000)
}
},
clearAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
this.refreshTimer = null
}
},
saveRefreshSetting() {
// 可持久化到localStorage
localStorage.setItem('orderDashboardAutoRefresh', JSON.stringify({
autoRefresh: this.autoRefresh,
refreshInterval: this.refreshInterval
}))
this.drawerVisible = false
this.startAutoRefresh()
},
loadRefreshSetting() {
const setting = localStorage.getItem('orderDashboardAutoRefresh')
if (setting) {
const { autoRefresh, refreshInterval } = JSON.parse(setting)
this.autoRefresh = autoRefresh
this.refreshInterval = refreshInterval
}
}
},
watch: {
autoRefresh(val) {
if (!val) {
this.clearAutoRefresh()
}
},
refreshInterval(val) {
if (this.autoRefresh) {
this.startAutoRefresh()
}
}
}
}
</script>
<style scoped>
.order-analysis-dashboard {
padding: 24px;
box-sizing: border-box;
}
.section-row {
margin-bottom: 30px;
}
.section-title {
margin-bottom: 20px;
padding: 0 10px;
}
.section-title h2 {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: #303133;
}
.section-title p {
margin: 0;
font-size: 14px;
color: #909399;
}
.top-row,
.chart-row {
margin-bottom: 20px;
}
.chart-row > .el-col {
display: flex;
flex-direction: column;
justify-content: stretch;
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<div>
<el-descriptions :column="3" title="财务状态" border>
<el-descriptions-item label="订单总金额">{{ order.orderAmount }}</el-descriptions-item>
<el-descriptions-item label="已收款金额">{{ receivedAmount }}</el-descriptions-item>
<el-descriptions-item label="未收款金额">{{ unreceivedAmount }}</el-descriptions-item>
<el-descriptions-item label="订单总金额">{{ order.orderAmount }}</el-descriptions-item>
<el-descriptions-item label="已收款金额">{{ receivedAmount }}</el-descriptions-item>
<el-descriptions-item label="未收款金额">{{ unreceivedAmount }}</el-descriptions-item>
</el-descriptions>
<el-descriptions title="收款明细"></el-descriptions>
@@ -137,8 +137,7 @@
</template>
<script>
import { listReceivable, getReceivable, delReceivable, addReceivable, updateReceivable, updatePaidAmount } from "@/api/finance/receivable";
import { updateOrder } from "@/api/crm/order";
import { listReceivable, getReceivable, delReceivable, addReceivable, updateReceivable, updatePaidAmount } from "@/api/finance/receivable"
export default {
name: "Receivable",

View File

@@ -242,12 +242,12 @@ export default {
Object.assign(item, calculateProductFields(item, 'quantity'));
});
this.products = products;
this.remark = data.remark || '';
this.remark = data.remark || '净边料/毛边料、简包/裸包、卷重结算';
this.productName = data.productName || '';
} catch (error) {
console.error('解析content失败:', error);
this.products = [{}];
this.remark = '';
this.remark = '净边料/毛边料、简包/裸包、卷重结算';
}
},
// 数量变更

View File

@@ -89,6 +89,9 @@
</el-col>
</el-row>
<el-form-item>
<el-checkbox v-model="autoFillOrderAmount">含税总额变化后自动填写订单总金额</el-checkbox>
</el-form-item>
<el-form-item label="产品内容">
<ProductContent v-model="form.productContent" :readonly="false" />
</el-form-item>
@@ -233,6 +236,8 @@ export default {
},
// 表单参数
form: {},
// 是否自动将含税总额填入订单总金额
autoFillOrderAmount: true,
// 导出预览
exportDialogVisible: false,
exportRow: null,
@@ -314,6 +319,19 @@ export default {
created() {
this.getDictList();
},
watch: {
'form.productContent': function (newVal) {
if (!this.autoFillOrderAmount || !newVal) return;
try {
const data = JSON.parse(newVal);
if (data.totalTaxTotal != null) {
this.form.orderAmount = data.totalTaxTotal;
}
} catch (e) {
// ignore parse errors
}
}
},
methods: {
/** 处理客户选择 */
handleCustomerChange(customer) {

View File

@@ -1,202 +0,0 @@
<template>
<div style="padding: 20px;">
<el-row :gutter="20">
<el-col :span="6">
<div>
<el-row>
<el-input v-model="queryParams.orderCode" placeholder="请输入单据编号" clearable
@keyup.enter.native="handleQuery" />
</el-row>
<klp-list :list-data="orderList" list-key="orderId" :loading="loading" @item-click="handleItemClick"
info1-field="orderCode" info1-max-percent="40" info5-field="createTime" info4-field="taxAmount">
<!-- info4 插槽Vue2 slot 指定插槽名slot-scope 接收作用域变量 -->
<template slot="info4" slot-scope="{ item }">
<span class="info-value info-value--primary">
{{ item.taxAmount }}含税
</span>
</template>
<!-- info1 插槽同理修改插槽语法 -->
<template slot="info1" slot-scope="{ item }">
<span class="info-value info-value--primary">
{{ item.salesManager }}{{ item.orderCode }}
</span>
</template>
<!-- info2 插槽dict-tag 若为 Vue2 兼容组件用法不变 -->
<template slot="info2" slot-scope="{ item }">
<dict-tag :options="dict.type.order_status" :value="item.orderStatus" />
</template>
<!-- actions 插槽el-button Element UI Vue2 组件用法不变 -->
<template slot="actions" slot-scope="{ item }">
<el-button size="small" type="text" style="color: red" icon="el-icon-delete"
@click.stop="handleDelete(item)"></el-button>
</template>
</klp-list>
<pagination :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" :total="total"
layout="prev, pager, next" @pagination="getList" />
</div>
</el-col>
<el-col :span="18" v-loading="rightLoading">
<el-tabs v-if="currentOrder" v-model="activeTab">
<el-tab-pane label="订单信息" name="orderDetail">
<el-form>
<el-form-item label="订单ID">
<el-input v-model="currentOrder.orderId" disabled />
</el-form-item>
<el-form-item label="订单编号">
<el-input v-model="currentOrder.orderCode" disabled />
</el-form-item>
<el-form-item label="销售经理">
<el-input v-model="currentOrder.salesManager" disabled />
</el-form-item>
<el-form-item label="客户">
<el-input v-model="currentOrder.customerName" disabled />
</el-form-item>
<el-form-item label="含税金额">
<el-input v-model="currentOrder.taxAmount" disabled />
</el-form-item>
<el-form-item label="无税金额">
<el-input v-model="currentOrder.noTaxAmount" disabled />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="订单明细" name="orderList">
<order-detail-list :orderId="currentOrder.orderId" :editable="false" />
</el-tab-pane>
<el-tab-pane label="应收明细" name="receivable">
<div style="margin-top: 10px;">
<ReceiveTable :order-id="currentOrder.orderId" :searchable="false" />
</div>
</el-tab-pane>
<el-tab-pane label="应付明细" name="payable">
<div style="margin-top: 10px;">
<PayTable :order-id="currentOrder.orderId" :searchable="false" />
</div>
</el-tab-pane>
<el-tab-pane label="凭证管理" name="document">
<KLPTable :data="currentOrder.documents" style="width: 100%" empty-text="暂无数据">
<el-table-column prop="docNo" label="凭证编号" />
<el-table-column prop="docDate" label="凭证日期" />
<el-table-column prop="amount" label="凭证金额" />
<el-table-column prop="status" label="凭证状态" />
</KLPTable>
</el-tab-pane>
</el-tabs>
<el-empty v-else description="请选择订单" />
</el-col>
</el-row>
</div>
</template>
<script>
import { listOrder } from "@/api/wms/order";
import { listOrderDetail } from "@/api/wms/orderDetail";
import { listReceivable } from "@/api/finance/receivable";
import { listPayable } from "@/api/finance/payable";
import { listFinancialDocument } from "@/api/finance/financialDocument";
import OrderDetailList from '@/views/wms/order/panels/detail.vue'
import klpList from "@/components/KLPUI/KLPList/index.vue"; // 引入klp-list组件
import ReceiveTable from '../components/ReceiveTable.vue';
import PayTable from '../components/PayTable.vue';
export default {
name: "Order",
components: {
OrderDetailList,
klpList, // 注册klp-list组件
ReceiveTable,
PayTable
},
dicts: ['order_status'],
data() {
return {
activeTab: "orderDetail",
currentOrder: null,
queryParams: {
pageNum: 1,
pageSize: 20,
orderCode: undefined
},
orderListLoading: false,
orderList: [], // 用于klp-list的列表数据
total: 0,
rightLoading: false,
}
},
mounted() {
this.getList();
},
methods: {
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
getList() {
this.orderListLoading = true;
listOrder(this.queryParams).then(response => {
this.orderList = response.rows; // 直接使用原始数据
this.total = response.total;
}).finally(() => {
this.orderListLoading = false;
});
},
// 处理列表项点击事件
handleItemClick(selectedItem) {
if (!selectedItem) {
this.currentOrder = null;
return;
}
if (this.rightLoading) {
this.$message.warning('请等待当前订单加载完成');
return;
}
this.currentOrder = selectedItem;
this.fetchData(selectedItem.orderId);
},
// 查看详情按钮点击事件
handleViewDetail(item) {
this.currentOrder = item;
this.fetchData(item.orderId);
},
async fetchData(orderId) {
this.rightLoading = true;
try {
// 并行请求提高性能
const [
orderDetailRes,
receivableRes,
payableRes,
documentRes
] = await Promise.all([
listOrderDetail({ orderId, pageSize: 1000 }),
listReceivable({ orderId, pageSize: 1000 }),
listPayable({ orderId, pageSize: 1000 }),
listFinancialDocument({ orderId, pageSize: 1000 })
]);
this.currentOrder = {
...this.currentOrder,
details: orderDetailRes.rows,
receivables: receivableRes.rows,
payables: payableRes.rows,
documents: documentRes.rows
};
} catch (error) {
this.$message.error('数据加载失败,请重试');
console.error(error);
} finally {
this.rightLoading = false;
}
}
}
}
</script>

View File

@@ -0,0 +1,877 @@
<template>
<div class="app-container">
<div class="date-range-section">
<TimeRangePicker v-model="dateRangeParams" startKey="startDate" endKey="endDate"
:defaultStartTime="defaultStartTime" :defaultEndTime="defaultEndTime" format="yyyy-MM-dd" @change="handleDateRangeChange"
@quick-select="getList" />
</div>
<div class="operation-bar">
<el-button type="primary" plain icon="el-icon-search" @click="handleQuery">搜索</el-button>
<el-button type="warning" plain icon="el-icon-download" @click="handleExport">导出</el-button>
</div>
<div class="filter-row">
<el-input v-model="queryParams.employeeName" placeholder="请输入员工姓名" clearable class="filter-input" @keyup.enter.native="handleQuery" />
<!-- <el-select v-model="queryParams.overallStatus" placeholder="请选择状态" clearable @change="handleQuery" class="filter-select">
<el-option label="迟到/早退/缺卡" value="abnormal" />
<el-option label="半天旷工" value="absent_half" />
<el-option label="全天旷工" value="absent_full" />
</el-select> -->
</div>
<div class="dept-filter-section" v-if="departmentList.length > 0">
<el-radio-group v-model="selectedDept" @change="handleDeptChange">
<el-radio-button label="">全部{{ totalEmployeeCount }}</el-radio-button>
<el-radio-button v-for="item in departmentList" :key="item.deptName" :label="item.deptName">
{{ item.deptName }}{{ item.count }}
</el-radio-button>
</el-radio-group>
</div>
<el-alert type="info" title="提示:点击修改按钮可查看考勤详情并调整考勤结果"></el-alert>
<el-table v-loading="loading" :data="abnormalList" border stripe>
<el-table-column label="员工姓名" align="center" prop="employeeName" width="100" />
<el-table-column label="日期" align="center" prop="workDate" width="100" />
<el-table-column label="总体状态" align="center" width="120">
<template slot-scope="scope">
<span :class="getStatusClass(scope.row.overallStatus)">{{ getStatusText(scope.row.overallStatus) }}</span>
</template>
</el-table-column>
<el-table-column label="时段一状态" align="center" prop="p1Status" width="100">
<template slot-scope="scope">
<span v-if="scope.row.p1Status" :class="getStatusClass(scope.row.p1Status)">{{ getStatusText(scope.row.p1Status) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="迟到(分)" align="center" prop="p1LateMinutes" width="80">
<template slot-scope="scope">
<span v-if="scope.row.p1LateMinutes > 0" class="late-info">{{ scope.row.p1LateMinutes }}</span>
<span v-else>0</span>
</template>
</el-table-column>
<el-table-column label="早退(分)" align="center" prop="p1EarlyMinutes" width="80">
<template slot-scope="scope">
<span v-if="scope.row.p1EarlyMinutes > 0" class="early-info">{{ scope.row.p1EarlyMinutes }}</span>
<span v-else>0</span>
</template>
</el-table-column>
<el-table-column label="扣款" align="center" width="80">
<template slot-scope="scope">¥{{ scope.row.p1Deduct || '0.00' }}</template>
</el-table-column>
<el-table-column label="时段二状态" align="center" prop="p2Status" width="100">
<template slot-scope="scope">
<span v-if="scope.row.p2Status" :class="getStatusClass(scope.row.p2Status)">{{ getStatusText(scope.row.p2Status) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="迟到(分)" align="center" prop="p2LateMinutes" width="80">
<template slot-scope="scope">
<span v-if="scope.row.p2LateMinutes > 0" class="late-info">{{ scope.row.p2LateMinutes }}</span>
<span v-else>0</span>
</template>
</el-table-column>
<el-table-column label="早退(分)" align="center" prop="p2EarlyMinutes" width="80">
<template slot-scope="scope">
<span v-if="scope.row.p2EarlyMinutes > 0" class="early-info">{{ scope.row.p2EarlyMinutes }}</span>
<span v-else>0</span>
</template>
</el-table-column>
<el-table-column label="扣款" align="center" width="80">
<template slot-scope="scope">¥{{ scope.row.p2Deduct || '0.00' }}</template>
</el-table-column>
<el-table-column label="旷工类型" align="center" width="100">
<template slot-scope="scope">
<span v-if="scope.row.absentType === 'half_day'">半天旷工</span>
<span v-else-if="scope.row.absentType === 'full_day'">全天旷工</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="连续旷工天数" align="center" prop="continuousAbsentDays" width="110" />
<el-table-column label="总扣款" align="center" width="90">
<template slot-scope="scope">
<span class="deduct-amount">¥{{ scope.row.totalDeduct || '0.00' }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="80" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="考勤详情" :visible.sync="detailDialogVisible" width="700px">
<el-form v-if="currentDetail" :model="editForm" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="员工姓名">
<span>{{ currentDetail.employeeName }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="日期">
<span>{{ currentDetail.workDate || currentDetail.startDate }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="班次名称">
<el-input v-if="isEdit" v-model="editForm.shiftName" placeholder="请输入班次名称" />
<span v-else>{{ currentDetail.shiftName || '-' }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="班次类型">
<el-select v-if="isEdit" v-model="editForm.shiftType" placeholder="请选择班次类型">
<el-option label="白班" value="白班" />
<el-option label="夜班" value="夜班" />
</el-select>
<span v-else>{{ currentDetail.shiftType || '-' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="总体状态">
<el-select v-if="isEdit" v-model="editForm.overallStatus" placeholder="请选择状态">
<el-option label="正常" value="normal" />
<el-option label="迟到/早退/缺卡" value="abnormal" />
<el-option label="半天旷工" value="absent_half" />
<el-option label="全天旷工" value="absent_full" />
</el-select>
<span v-else :class="getStatusClass(currentDetail.overallStatus)">{{ getStatusText(currentDetail.overallStatus) }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="旷工类型">
<el-select v-if="isEdit" v-model="editForm.absentType" placeholder="请选择旷工类型">
<el-option label="半天旷工" value="half_day" />
<el-option label="全天旷工" value="full_day" />
</el-select>
<span v-else>{{ currentDetail.absentType ? (currentDetail.absentType === 'half_day' ? '半天旷工' : '全天旷工') : '-' }}</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="连续旷工天数">
<el-input v-if="isEdit" v-model="editForm.continuousAbsentDays" type="number" placeholder="请输入天数" />
<span v-else>{{ currentDetail.continuousAbsentDays || '0' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="总扣款金额">
<el-input v-if="isEdit" v-model="editForm.totalDeduct" type="number" placeholder="请输入金额" />
<span v-else class="deduct-amount">¥{{ currentDetail.totalDeduct || '0.00' }}</span>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="备注">
<el-input v-if="isEdit" v-model="editForm.remark" placeholder="请输入备注" />
<span v-else>{{ currentDetail.remark || '-' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">时段一</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="理论上班">
<el-time-picker style="width: 100%" v-if="isEdit" v-model="editForm.p1StartTime" value-format="HH:mm:ss" placeholder="选择时间" />
<span v-else>{{ formatTime(currentDetail.p1StartTime) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="理论下班">
<el-time-picker style="width: 100%" v-if="isEdit" v-model="editForm.p1EndTime" value-format="HH:mm:ss" placeholder="选择时间" />
<span v-else>{{ formatTime(currentDetail.p1EndTime) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际上班">
<span>{{ formatTime(currentDetail.p1FirstCheck) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际下班">
<span>{{ formatTime(currentDetail.p1LastCheck) }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="状态">
<el-select v-if="isEdit" v-model="editForm.p1Status" placeholder="请选择状态">
<el-option label="正常" value="normal" />
<el-option label="迟到预警" value="late_warn" />
<el-option label="迟到I" value="late_one" />
<el-option label="迟到II" value="late_two" />
<el-option label="早退预警" value="early_warn" />
<el-option label="早退I" value="early_one" />
<el-option label="早退II" value="early_two" />
<el-option label="半天旷工" value="absent_half" />
<el-option label="未打卡" value="missed" />
</el-select>
<span v-else :class="getStatusClass(currentDetail.p1Status)">{{ getStatusText(currentDetail.p1Status) }}</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="迟到分钟">
<el-input v-if="isEdit" v-model="editForm.p1LateMinutes" type="number" placeholder="请输入分钟数" />
<span v-else>{{ currentDetail.p1LateMinutes || '0' }}</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="早退分钟">
<el-input v-if="isEdit" v-model="editForm.p1EarlyMinutes" type="number" placeholder="请输入分钟数" />
<span v-else>{{ currentDetail.p1EarlyMinutes || '0' }}</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="扣款金额">
<el-input v-if="isEdit" v-model="editForm.p1Deduct" type="number" placeholder="请输入金额" />
<span v-else>¥{{ currentDetail.p1Deduct || '0.00' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left" v-if="currentDetail.p2StartTime || isEdit">时段二</el-divider>
<el-row v-if="currentDetail.p2StartTime || isEdit">
<el-col :span="12">
<el-form-item label="理论上班">
<el-time-picker style="width: 100%" v-if="isEdit" v-model="editForm.p2StartTime" value-format="HH:mm:ss" placeholder="选择时间" />
<span v-else>{{ formatTime(currentDetail.p2StartTime) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="理论下班">
<el-time-picker style="width: 100%" v-if="isEdit" v-model="editForm.p2EndTime" value-format="HH:mm:ss" placeholder="选择时间" />
<span v-else>{{ formatTime(currentDetail.p2EndTime) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际上班">
<span>{{ formatTime(currentDetail.p2FirstCheck) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际下班">
<span>{{ formatTime(currentDetail.p2LastCheck) }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row v-if="currentDetail.p2StartTime || isEdit">
<el-col :span="6">
<el-form-item label="状态">
<el-select v-if="isEdit" v-model="editForm.p2Status" placeholder="请选择状态">
<el-option label="正常" value="normal" />
<el-option label="迟到预警" value="late_warn" />
<el-option label="迟到I" value="late_one" />
<el-option label="迟到II" value="late_two" />
<el-option label="早退预警" value="early_warn" />
<el-option label="早退I" value="early_one" />
<el-option label="早退II" value="early_two" />
<el-option label="半天旷工" value="absent_half" />
<el-option label="未打卡" value="missed" />
</el-select>
<span v-else :class="getStatusClass(currentDetail.p2Status)">{{ getStatusText(currentDetail.p2Status) }}</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="迟到分钟">
<el-input v-if="isEdit" v-model="editForm.p2LateMinutes" type="number" placeholder="请输入分钟数" />
<span v-else>{{ currentDetail.p2LateMinutes || '0' }}</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="早退分钟">
<el-input v-if="isEdit" v-model="editForm.p2EarlyMinutes" type="number" placeholder="请输入分钟数" />
<span v-else>{{ currentDetail.p2EarlyMinutes || '0' }}</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="扣款金额">
<el-input v-if="isEdit" v-model="editForm.p2Deduct" type="number" placeholder="请输入金额" />
<span v-else>¥{{ currentDetail.p2Deduct || '0.00' }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-divider content-position="left">请假记录</el-divider>
<div class="dialog-filter-bar">
<el-input v-model="detailLeaveQuery.applicantName" placeholder="请假人姓名" class="dialog-filter-input" />
<el-button type="primary" plain size="small" icon="el-icon-search" @click="getDetailLeaveList">搜索</el-button>
</div>
<el-table v-loading="detailLeaveLoading" :data="detailLeaveList" border stripe size="small">
<el-table-column prop="leaveType" label="请假类型" width="100" />
<el-table-column prop="startTime" label="开始时间" width="150" />
<el-table-column prop="endTime" label="结束时间" width="150" />
<el-table-column prop="leaveDays" label="小时" width="80" />
<el-table-column prop="leaveReason" label="原因" />
<el-table-column prop="approvalStatus" label="状态" width="100">
<template slot-scope="scope">
<span :class="getApprovalStatusClass(scope.row.approvalStatus)">{{ getApprovalStatusText(scope.row.approvalStatus) }}</span>
</template>
</el-table-column>
</el-table>
<el-divider content-position="left">外出记录</el-divider>
<div class="dialog-filter-bar">
<el-input v-model="detailOutQuery.applicantName" placeholder="外出人姓名" class="dialog-filter-input" />
<el-button type="primary" plain size="small" icon="el-icon-search" @click="getDetailOutList">搜索</el-button>
</div>
<el-table v-loading="detailOutLoading" :data="detailOutList" border stripe size="small">
<el-table-column prop="outType" label="外出类型" width="100" />
<el-table-column prop="startTime" label="开始时间" width="150" />
<el-table-column prop="endTime" label="结束时间" width="150" />
<el-table-column prop="outHours" label="时长(小时)" width="100" />
<el-table-column prop="outPlace" label="地点" />
<el-table-column prop="approvalStatus" label="状态" width="100">
<template slot-scope="scope">
<span :class="getApprovalStatusClass(scope.row.approvalStatus)">{{ getApprovalStatusText(scope.row.approvalStatus) }}</span>
</template>
</el-table-column>
</el-table>
<el-divider content-position="left">打卡记录</el-divider>
<el-table v-loading="detailRecordsLoading" :data="detailRecordsList" border stripe size="small">
<el-table-column prop="ename" label="姓名" width="100" />
<el-table-column prop="deptname" label="部门" />
<el-table-column prop="checktime" label="打卡时间">
<template slot-scope="scope">
<span>{{ scope.row.checktime }}</span>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button v-if="!isEdit" type="primary" @click="handleEdit">编辑</el-button>
<el-button v-if="isEdit" @click="cancelEdit">取消</el-button>
<el-button v-if="isEdit" type="primary" @click="submitEdit">保存</el-button>
<el-button @click="closeDetail">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listAttendanceCheck, getAttendanceCheck, updateAttendanceCheck } from "@/api/wms/attendanceCheck";
import { listOutRequest } from "@/api/wms/outRequest";
import { listLeaveRequest } from "@/api/wms/leaveRequest";
import { listRecords } from "@/api/wms/attendance";
import { listEmployeeInfo } from "@/api/wms/employeeInfo";
import TimeRangePicker from "@/views/wms/report/components/timeRangePicker";
export default {
name: "AttendanceAbnormal",
components: {
TimeRangePicker
},
data() {
return {
loading: false,
abnormalList: [],
dateRangeParams: {},
defaultStartTime: '',
defaultEndTime: '',
queryParams: {
employeeName: undefined,
startDate: undefined,
endDate: undefined,
overallStatus: undefined,
abnormal: true
},
allEmployees: [],
departmentList: [],
selectedDept: '',
currentDeptEmployeeIds: '',
detailDialogVisible: false,
currentDetail: null,
isEdit: false,
editForm: {},
detailLeaveLoading: false,
detailLeaveList: [],
detailLeaveQuery: {
applicantName: ''
},
detailOutLoading: false,
detailOutList: [],
detailOutQuery: {
applicantName: ''
},
detailRecordsLoading: false,
detailRecordsList: []
};
},
created() {
this.getAllEmployees().then(() => {
this.initDateRange();
});
},
computed: {
totalEmployeeCount() {
return this.allEmployees.length;
}
},
methods: {
initDateRange() {
const now = new Date()
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1)
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0)
this.defaultStartTime = this.formatDate(firstDay)
this.defaultEndTime = this.formatDate(lastDay)
this.dateRangeParams = {
startDate: this.defaultStartTime,
endDate: this.defaultEndTime
}
this.queryParams.startDate = this.defaultStartTime
this.queryParams.endDate = this.defaultEndTime
this.getList()
},
getAllEmployees() {
return listEmployeeInfo({
pageNum: 1,
pageSize: 10000
}).then(res => {
this.allEmployees = res.rows || []
const deptMap = {}
this.allEmployees.forEach(emp => {
const deptName = emp.dept || '未分配部门'
if (!deptMap[deptName]) {
deptMap[deptName] = new Set()
}
deptMap[deptName].add(String(emp.infoId))
})
this.departmentList = Object.keys(deptMap).map(deptName => {
const ids = [...deptMap[deptName]]
return {
deptName,
count: ids.length,
empIds: ids.join(',')
}
})
if (this.departmentList.length > 0) {
this.selectedDept = ''
this.currentDeptEmployeeIds = ''
}
})
},
handleDeptChange(deptName) {
if (!deptName) {
this.currentDeptEmployeeIds = ''
} else {
const dept = this.departmentList.find(d => d.deptName === deptName)
this.currentDeptEmployeeIds = dept ? dept.empIds : ''
}
this.getList()
},
formatDate(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}-${month}-${day}`
},
handleDateRangeChange() {
if (this.dateRangeParams.startDate && this.dateRangeParams.endDate) {
this.queryParams.startDate = this.dateRangeParams.startDate
this.queryParams.endDate = this.dateRangeParams.endDate
this.getList()
}
},
getList() {
this.loading = true;
const params = { ...this.queryParams };
if (this.currentDeptEmployeeIds) {
params.userIds = this.currentDeptEmployeeIds.split(',');
}
listAttendanceCheck(params).then(response => {
this.abnormalList = response.rows || [];
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
handleQuery() {
this.getList();
},
handleExport() {
const params = { ...this.queryParams, abnormal: true };
if (this.currentDeptEmployeeIds) {
params.userIds = this.currentDeptEmployeeIds.split(',');
}
this.download('wms/attendanceCheck/export', params, `attendanceAbnormal_${new Date().getTime()}.xlsx`);
},
handleUpdate(row) {
this.loading = true;
getAttendanceCheck(row.checkId).then(response => {
this.currentDetail = response.data;
this.detailLeaveQuery.applicantName = row.employeeName;
this.detailOutQuery.applicantName = row.employeeName;
this.getDetailLeaveList();
this.getDetailOutList();
this.getDetailRecords(row.employeeName, row.startDate);
this.detailDialogVisible = true;
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
closeDetail() {
this.detailDialogVisible = false;
this.currentDetail = null;
this.isEdit = false;
},
handleEdit() {
this.isEdit = true;
this.editForm = { ...this.currentDetail };
this.initEditForm();
},
initEditForm() {
if (this.currentDetail.p1StartTime) {
this.editForm.p1StartTime = this.convertToTimeString(this.currentDetail.p1StartTime);
}
if (this.currentDetail.p1EndTime) {
this.editForm.p1EndTime = this.convertToTimeString(this.currentDetail.p1EndTime);
}
if (this.currentDetail.p1FirstCheck) {
this.editForm.p1FirstCheck = this.convertToDateTimeString(this.currentDetail.p1FirstCheck);
}
if (this.currentDetail.p1LastCheck) {
this.editForm.p1LastCheck = this.convertToDateTimeString(this.currentDetail.p1LastCheck);
}
if (this.currentDetail.p2StartTime) {
this.editForm.p2StartTime = this.convertToTimeString(this.currentDetail.p2StartTime);
}
if (this.currentDetail.p2EndTime) {
this.editForm.p2EndTime = this.convertToTimeString(this.currentDetail.p2EndTime);
}
if (this.currentDetail.p2FirstCheck) {
this.editForm.p2FirstCheck = this.convertToDateTimeString(this.currentDetail.p2FirstCheck);
}
if (this.currentDetail.p2LastCheck) {
this.editForm.p2LastCheck = this.convertToDateTimeString(this.currentDetail.p2LastCheck);
}
},
convertToTimeString(dateStr) {
if (!dateStr) return ''
if (dateStr instanceof Date) {
return dateStr.toTimeString().slice(0, 8)
}
const str = String(dateStr)
if (str.length >= 8) {
const parts = str.split(/[-T:\s]/)
if (parts.length >= 6) {
return `${parts[3]}:${parts[4]}:${parts[5]}`
} else if (str.includes(':')) {
const timePart = str.split(' ').pop()
return timePart.length >= 8 ? timePart : timePart.padEnd(8, ':00')
}
}
return str
},
convertToDateTimeString(dateStr) {
if (!dateStr) return ''
if (dateStr instanceof Date) {
const d = dateStr
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const str = String(dateStr)
if (str.includes('T')) {
return str.replace('T', ' ')
}
return str
},
cancelEdit() {
this.isEdit = false;
this.editForm = {};
},
submitEdit() {
this.loading = true;
updateAttendanceCheck(this.editForm).then(response => {
this.$modal.msgSuccess("修改成功");
this.isEdit = false;
this.editForm = {};
this.getList();
this.closeDetail();
}).finally(() => {
this.loading = false;
});
},
formatTime(time) {
if (!time) return '-'
return time
},
getDetailLeaveList() {
this.detailLeaveLoading = true;
const query = {
applicantName: this.detailLeaveQuery.applicantName,
startTime: this.queryParams.startDate,
endTime: this.queryParams.endDate
};
listLeaveRequest(query).then(response => {
this.detailLeaveList = response.rows || response.data || [];
this.detailLeaveLoading = false;
}).catch(() => {
this.detailLeaveLoading = false;
});
},
getDetailOutList() {
this.detailOutLoading = true;
const query = {
applicantName: this.detailOutQuery.applicantName,
startTime: this.queryParams.startDate,
endTime: this.queryParams.endDate
};
listOutRequest(query).then(response => {
this.detailOutList = response.rows || response.data || [];
this.detailOutLoading = false;
}).catch(() => {
this.detailOutLoading = false;
});
},
getDetailRecords(ename, date) {
this.detailRecordsLoading = true;
let dateStr = '';
if (date) {
if (date instanceof Date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
dateStr = `${y}-${m}-${d}`;
} else if (typeof date === 'string') {
dateStr = date.split(' ')[0];
}
}
if (!dateStr) {
dateStr = this.queryParams.startDate || '';
}
if (!dateStr) {
this.detailRecordsLoading = false;
return;
}
listRecords({
pageNum: 1,
pageSize: 100,
ename,
checktimeStart: dateStr + ' 00:00:00',
checktimeEnd: dateStr + ' 23:59:59'
}).then(response => {
this.detailRecordsList = response.rows || [];
this.detailRecordsLoading = false;
}).catch(() => {
this.detailRecordsLoading = false;
});
},
getApprovalStatusClass(status) {
switch (status) {
case '待审批': return 'approval-pending'
case '已同意': return 'approval-approved'
case '已驳回': return 'approval-rejected'
case '已撤销': return 'approval-canceled'
default: return 'approval-default'
}
},
getApprovalStatusText(status) {
switch (status) {
case '待审批': return '待审批'
case '已同意': return '已同意'
case '已驳回': return '已驳回'
case '已撤销': return '已撤销'
default: return status || '-'
}
},
getStatusClass(status) {
switch (status) {
case 'normal': return 'status-normal'
case 'late_warn':
case 'late_one':
case 'late_two': return 'status-late'
case 'early_warn':
case 'early_one':
case 'early_two': return 'status-early'
case 'absent_half': return 'status-absent-half'
case 'absent_full': return 'status-absent-full'
case 'abnormal': return 'status-abnormal'
case 'missed': return 'status-missed'
case 'missed_start': return 'status-missed'
case 'missed_end': return 'status-missed'
default: return 'status-default'
}
},
getStatusText(status) {
switch (status) {
case 'normal': return '正常'
case 'late_warn': return '迟到预警'
case 'late_one': return '迟到I'
case 'late_two': return '迟到II'
case 'early_warn': return '早退预警'
case 'early_one': return '早退I'
case 'early_two': return '早退II'
case 'absent_half': return '半天旷工'
case 'absent_full': return '全天旷工'
case 'abnormal': return '迟到/早退/缺卡'
case 'missed': return '未打卡'
case 'missed_start': return '上班漏打卡'
case 'missed_end': return '下班漏打卡'
default: return '-'
}
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
}
.date-range-section {
margin-bottom: 20px;
}
.operation-bar {
margin-bottom: 20px;
}
.filter-row {
margin-bottom: 15px;
display: flex;
gap: 10px;
}
.filter-input {
width: 200px;
}
.filter-select {
width: 180px;
}
.dept-filter-section {
margin-bottom: 20px;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.late-info {
color: #e6a23c;
font-weight: bold;
}
.early-info {
color: #f56c6c;
font-weight: bold;
}
.deduct-amount {
color: #f56c6c;
font-weight: bold;
}
.status-normal {
background-color: #67c23a;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-late {
background-color: #e6a23c;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-early {
background-color: #f56c6c;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-absent-half {
background-color: #909399;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-absent-full {
background-color: #606266;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-abnormal {
background-color: #f56c6c;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-missed {
background-color: #e6a23c;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-default {
background-color: #c0c4cc;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.dialog-filter-bar {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.dialog-filter-input {
width: 150px;
margin-right: 10px;
}
.approval-pending {
color: #e6a23c;
}
.approval-approved {
color: #67c23a;
}
.approval-rejected {
color: #f56c6c;
}
.approval-canceled {
color: #909399;
}
.approval-default {
color: #606266;
}
</style>