Commit d4a6b78f23d995424428e0f576def1096d4026eb

Authored by wuxw
1 parent 809d9ffa

OA 中考勤功能开发完成

Showing 41 changed files with 4083 additions and 309 deletions
src/api/oa/addAttendanceClassesStaffApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * 保存考勤班次员工信息
  5 + * @param {Object} data 员工信息
  6 + * @returns {Promise}
  7 + */
  8 +export function saveAttendanceClassesStaff(data) {
  9 + return new Promise((resolve, reject) => {
  10 + request({
  11 + url: '/attendanceClasses.saveAttendanceClassesStaff',
  12 + method: 'post',
  13 + data
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +/**
  24 + * 上传图片
  25 + * @param {FormData} formData 包含图片文件的FormData
  26 + * @returns {Promise}
  27 + */
  28 +export function uploadFile(formData) {
  29 + return new Promise((resolve, reject) => {
  30 + request({
  31 + url: '/uploadImage',
  32 + method: 'post',
  33 + data: formData,
  34 + headers: {
  35 + 'Content-Type': 'multipart/form-data'
  36 + }
  37 + }).then(response => {
  38 + const res = response.data
  39 + resolve(res)
  40 + }).catch(error => {
  41 + reject(error)
  42 + })
  43 + })
  44 +}
  45 +
  46 +/**
  47 + * 查询员工信息
  48 + * @param {Object} params 查询参数
  49 + * @returns {Promise}
  50 + */
  51 +export function queryStaffInfos(params) {
  52 + return new Promise((resolve, reject) => {
  53 + request({
  54 + url: '/query.staff.infos',
  55 + method: 'get',
  56 + params
  57 + }).then(response => {
  58 + const res = response.data
  59 + resolve(res)
  60 + }).catch(error => {
  61 + reject(error)
  62 + })
  63 + })
  64 +}
  65 +
  66 +/**
  67 + * 获取组织树
  68 + * @param {Object} params 查询参数
  69 + * @returns {Promise}
  70 + */
  71 +export function listOrgTree(params) {
  72 + return new Promise((resolve, reject) => {
  73 + request({
  74 + url: '/org.listOrgTree',
  75 + method: 'get',
  76 + params
  77 + }).then(response => {
  78 + const res = response.data
  79 + resolve(res)
  80 + }).catch(error => {
  81 + reject(error)
  82 + })
  83 + })
  84 +}
  85 +
  86 +/**
  87 + * 下载文件
  88 + * @param {Object} params 文件参数
  89 + * @returns {Promise}
  90 + */
  91 +export function downloadFile(params) {
  92 + return new Promise((resolve, reject) => {
  93 + request({
  94 + url: '/callComponent/download/getFile/file',
  95 + method: 'get',
  96 + params
  97 + }).then(response => {
  98 + const res = response.data
  99 + resolve(res)
  100 + }).catch(error => {
  101 + reject(error)
  102 + })
  103 + })
  104 +}
0 \ No newline at end of file 105 \ No newline at end of file
src/api/oa/attendanceClassesManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 获取考勤班组列表
  5 +export function listAttendanceClassess(params) {
  6 + return new Promise((resolve, reject) => {
  7 + request({
  8 + url: '/attendanceClasses.listAttendanceClassess',
  9 + method: 'get',
  10 + params: {
  11 + ...params,
  12 + communityId: getCommunityId()
  13 + }
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +// 添加考勤班组
  24 +export function saveAttendanceClasses(data) {
  25 + return new Promise((resolve, reject) => {
  26 + request({
  27 + url: '/attendanceClasses.saveAttendanceClasses',
  28 + method: 'post',
  29 + data: {
  30 + ...data,
  31 + communityId: getCommunityId()
  32 + }
  33 + }).then(response => {
  34 + const res = response.data
  35 + resolve(res)
  36 + }).catch(error => {
  37 + reject(error)
  38 + })
  39 + })
  40 +}
  41 +
  42 +// 修改考勤班组
  43 +export function updateAttendanceClasses(data) {
  44 + return new Promise((resolve, reject) => {
  45 + request({
  46 + url: '/attendanceClasses.updateAttendanceClasses',
  47 + method: 'post',
  48 + data: {
  49 + ...data,
  50 + communityId: getCommunityId()
  51 + }
  52 + }).then(response => {
  53 + const res = response.data
  54 + resolve(res)
  55 + }).catch(error => {
  56 + reject(error)
  57 + })
  58 + })
  59 +}
  60 +
  61 +// 删除考勤班组
  62 +export function deleteAttendanceClasses(data) {
  63 + return new Promise((resolve, reject) => {
  64 + request({
  65 + url: '/attendanceClasses.deleteAttendanceClasses',
  66 + method: 'post',
  67 + data: {
  68 + ...data,
  69 + communityId: getCommunityId()
  70 + }
  71 + }).then(response => {
  72 + const res = response.data
  73 + resolve(res)
  74 + }).catch(error => {
  75 + reject(error)
  76 + })
  77 + })
  78 +}
0 \ No newline at end of file 79 \ No newline at end of file
src/api/oa/attendanceClassesStaffManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取考勤班次列表
  4 +export function listAttendanceClassess(params) {
  5 + return new Promise((resolve, reject) => {
  6 + request({
  7 + url: '/attendanceClasses.listAttendanceClassess',
  8 + method: 'get',
  9 + params
  10 + }).then(response => {
  11 + const res = response.data
  12 + resolve(res)
  13 + }).catch(error => {
  14 + reject(error)
  15 + })
  16 + })
  17 +}
  18 +
  19 +// 获取考勤班次员工列表
  20 +export function listAttendanceClassesStaff(params) {
  21 + return new Promise((resolve, reject) => {
  22 + request({
  23 + url: '/attendanceClasses.listAttendanceClassesStaff',
  24 + method: 'get',
  25 + params
  26 + }).then(response => {
  27 + const res = response.data
  28 + resolve(res)
  29 + }).catch(error => {
  30 + reject(error)
  31 + })
  32 + })
  33 +}
  34 +
  35 +// 删除考勤班次员工
  36 +export function deleteAttendanceClassesStaff(data) {
  37 + return new Promise((resolve, reject) => {
  38 + request({
  39 + url: '/attendanceClasses.deleteAttendanceClassesStaff',
  40 + method: 'post',
  41 + data
  42 + }).then(response => {
  43 + const res = response.data
  44 + resolve(res)
  45 + }).catch(error => {
  46 + reject(error)
  47 + })
  48 + })
  49 +}
0 \ No newline at end of file 50 \ No newline at end of file
src/api/oa/attendanceLogManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * 查询考勤记录列表
  5 + * @param {Object} params 查询参数
  6 + * @param {string} params.classesName 班组名称
  7 + * @param {string} params.departmentName 部门名称
  8 + * @param {string} params.date 打卡日期
  9 + * @param {string} params.staffName 员工名称
  10 + * @param {number} params.page 当前页码
  11 + * @param {number} params.row 每页条数
  12 + * @returns {Promise} 考勤记录列表
  13 + */
  14 +export function queryAttendanceLog(params) {
  15 + return new Promise((resolve, reject) => {
  16 + request({
  17 + url: '/attendanceClass/queryAttendanceLog',
  18 + method: 'get',
  19 + params: {
  20 + ...params,
  21 + classesName: params.classesName.trim() || '',
  22 + departmentName: params.departmentName.trim() || ''
  23 + }
  24 + }).then(response => {
  25 + const res = response.data
  26 + resolve(res)
  27 + }).catch(error => {
  28 + reject(error)
  29 + })
  30 + })
  31 +}
0 \ No newline at end of file 32 \ No newline at end of file
src/api/oa/monthAttendanceManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取月考勤列表
  4 +export function getMonthAttendance(params) {
  5 + return new Promise((resolve, reject) => {
  6 + request({
  7 + url: '/attendanceClass/getMonthAttendance',
  8 + method: 'get',
  9 + params
  10 + }).then(response => {
  11 + const res = response.data
  12 + resolve(res)
  13 + }).catch(error => {
  14 + reject(error)
  15 + })
  16 + })
  17 +}
  18 +
  19 +// 获取考勤班次列表
  20 +export function getAttendanceClasses(params) {
  21 + return new Promise((resolve, reject) => {
  22 + request({
  23 + url: '/attendanceClasses.listAttendanceClassess',
  24 + method: 'get',
  25 + params
  26 + }).then(response => {
  27 + const res = response.data
  28 + resolve(res)
  29 + }).catch(error => {
  30 + reject(error)
  31 + })
  32 + })
  33 +}
  34 +
  35 +// 导出月考勤数据
  36 +export function exportMonthAttendance(params) {
  37 + return new Promise((resolve, reject) => {
  38 + request({
  39 + url: '/export.exportData',
  40 + method: 'get',
  41 + params
  42 + }).then(response => {
  43 + const res = response.data
  44 + resolve(res)
  45 + }).catch(error => {
  46 + reject(error)
  47 + })
  48 + })
  49 +}
0 \ No newline at end of file 50 \ No newline at end of file
src/api/oa/newOaWorkflowDoingApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +// 获取待办信息
  4 +export function getUndoInfo(params) {
  5 + return new Promise((resolve, reject) => {
  6 + request({
  7 + url: '/callComponent/undo/list',
  8 + method: 'get',
  9 + params
  10 + }).then(response => {
  11 + const res = response.data
  12 + resolve(res)
  13 + }).catch(error => {
  14 + reject(error)
  15 + })
  16 + })
  17 +}
  18 +
  19 +// 查询OA工作流列表
  20 +export function listNewOaWorkflows(params) {
  21 + return new Promise((resolve, reject) => {
  22 + request({
  23 + url: '/oaWorkflow/queryOaWorkflow',
  24 + method: 'get',
  25 + params
  26 + }).then(response => {
  27 + const res = response.data
  28 + resolve(res)
  29 + }).catch(error => {
  30 + reject(error)
  31 + })
  32 + })
  33 +}
0 \ No newline at end of file 34 \ No newline at end of file
src/api/oa/newOaWorkflowManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * 查询OA工作流列表
  5 + * @param {Object} params 查询参数
  6 + * @returns {Promise} Promise对象
  7 + */
  8 +export function queryOaWorkflow(params) {
  9 + return new Promise((resolve, reject) => {
  10 + request({
  11 + url: '/oaWorkflow/queryOaWorkflow',
  12 + method: 'get',
  13 + params
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +/**
  24 + * 检查工作流是否部署
  25 + * @param {Object} params 查询参数
  26 + * @returns {Promise} Promise对象
  27 + */
  28 +export function checkWorkflowDeployment(params) {
  29 + return queryOaWorkflow(params)
  30 +}
0 \ No newline at end of file 31 \ No newline at end of file
src/api/oa/staffAttendanceManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 查询员工信息列表
  5 +export function queryStaffInfos(params) {
  6 + return new Promise((resolve, reject) => {
  7 + request({
  8 + url: '/query.staff.infos',
  9 + method: 'get',
  10 + params: {
  11 + ...params,
  12 + communityId: getCommunityId()
  13 + }
  14 + }).then(response => {
  15 + resolve(response.data)
  16 + }).catch(error => {
  17 + reject(error)
  18 + })
  19 + })
  20 +}
  21 +
  22 +// 查询考勤任务
  23 +export function queryAttendanceClassesTask(params) {
  24 + return new Promise((resolve, reject) => {
  25 + request({
  26 + url: '/attendanceClass/queryAttendanceClassesTask',
  27 + method: 'get',
  28 + params: {
  29 + ...params,
  30 + communityId: getCommunityId()
  31 + }
  32 + }).then(response => {
  33 + resolve(response.data)
  34 + }).catch(error => {
  35 + reject(error)
  36 + })
  37 + })
  38 +}
  39 +
  40 +// 查询考勤日志
  41 +export function queryAttendanceLog(params) {
  42 + return new Promise((resolve, reject) => {
  43 + request({
  44 + url: '/attendanceClass/queryAttendanceLog',
  45 + method: 'get',
  46 + params: {
  47 + ...params,
  48 + communityId: getCommunityId()
  49 + }
  50 + }).then(response => {
  51 + resolve(response.data)
  52 + }).catch(error => {
  53 + reject(error)
  54 + })
  55 + })
  56 +}
  57 +
  58 +// 补考勤
  59 +export function attendanceReplenishCheckIn(data) {
  60 + return new Promise((resolve, reject) => {
  61 + request({
  62 + url: '/attendanceClasses.attendanceReplenishCheckIn',
  63 + method: 'post',
  64 + data,
  65 + headers: {
  66 + 'Content-Type': 'application/json'
  67 + }
  68 + }).then(response => {
  69 + resolve(response.data)
  70 + }).catch(error => {
  71 + reject(error)
  72 + })
  73 + })
  74 +}
  75 +
  76 +// 查询组织树
  77 +export function listOrgTree(params) {
  78 + return new Promise((resolve, reject) => {
  79 + request({
  80 + url: '/org.listOrgTree',
  81 + method: 'get',
  82 + params: {
  83 + ...params,
  84 + communityId: getCommunityId()
  85 + }
  86 + }).then(response => {
  87 + resolve(response.data)
  88 + }).catch(error => {
  89 + reject(error)
  90 + })
  91 + })
  92 +}
0 \ No newline at end of file 93 \ No newline at end of file
src/api/oa/todayAttendanceManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * 查询今日考勤列表
  5 + * @param {Object} params 查询参数
  6 + * @returns {Promise} Promise对象
  7 + */
  8 +export function queryAttendanceClassesTask(params) {
  9 + return new Promise((resolve, reject) => {
  10 + request({
  11 + url: '/attendanceClass/queryAttendanceClassesTask',
  12 + method: 'get',
  13 + params
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve({
  17 + data: res.data,
  18 + total: res.total,
  19 + records: res.records
  20 + })
  21 + }).catch(error => {
  22 + reject(error)
  23 + })
  24 + })
  25 +}
0 \ No newline at end of file 26 \ No newline at end of file
src/components/oa/addAttendanceClasses.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('addAttendanceClasses.title')" :visible.sync="visible" width="60%" @close="closeDialog">
  3 + <el-form ref="form" :model="formData" :rules="rules" label-width="150px" label-position="right">
  4 + <el-row>
  5 + <el-col :span="12">
  6 + <el-form-item :label="$t('addAttendanceClasses.classesName')" prop="classesName">
  7 + <el-input v-model="formData.classesName" :placeholder="$t('addAttendanceClasses.classesNamePlaceholder')" />
  8 + </el-form-item>
  9 + </el-col>
  10 + <el-col :span="12">
  11 + <el-form-item :label="$t('addAttendanceClasses.timeOffset')" prop="timeOffset">
  12 + <el-input v-model="formData.timeOffset" type="number"
  13 + :placeholder="$t('addAttendanceClasses.timeOffsetPlaceholder')">
  14 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  15 + </el-input>
  16 + </el-form-item>
  17 + </el-col>
  18 + </el-row>
  19 + <el-row>
  20 + <el-col :span="12">
  21 + <el-form-item :label="$t('addAttendanceClasses.lateOffset')" prop="lateOffset">
  22 + <el-input v-model="formData.lateOffset" type="number"
  23 + :placeholder="$t('addAttendanceClasses.lateOffsetPlaceholder')">
  24 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  25 + </el-input>
  26 + </el-form-item>
  27 + </el-col>
  28 + <el-col :span="12">
  29 + <el-form-item :label="$t('addAttendanceClasses.leaveOffset')" prop="leaveOffset">
  30 + <el-input v-model="formData.leaveOffset" type="number"
  31 + :placeholder="$t('addAttendanceClasses.leaveOffsetPlaceholder')">
  32 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  33 + </el-input>
  34 + </el-form-item>
  35 + </el-col>
  36 + </el-row>
  37 + <el-row>
  38 + <el-col :span="12">
  39 + <el-form-item :label="$t('addAttendanceClasses.maxLastOffset')" prop="maxLastOffset">
  40 + <el-input v-model="formData.maxLastOffset" type="number"
  41 + :placeholder="$t('addAttendanceClasses.maxLastOffsetPlaceholder')">
  42 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  43 + </el-input>
  44 + </el-form-item>
  45 + </el-col>
  46 + </el-row>
  47 + </el-form>
  48 + <div slot="footer" class="dialog-footer">
  49 + <el-button @click="closeDialog">{{ $t('common.cancel') }}</el-button>
  50 + <el-button type="primary" @click="saveAttendanceClassesInfo">{{ $t('common.save') }}</el-button>
  51 + </div>
  52 + </el-dialog>
  53 +</template>
  54 +
  55 +<script>
  56 +import { saveAttendanceClasses } from '@/api/oa/attendanceClassesManageApi'
  57 +import { getCommunityId } from '@/api/community/communityApi'
  58 +
  59 +export default {
  60 + name: 'AddAttendanceClasses',
  61 + data() {
  62 + return {
  63 + visible: false,
  64 + formData: {
  65 + classesId: '',
  66 + classesName: '',
  67 + timeOffset: '',
  68 + leaveOffset: '',
  69 + lateOffset: '',
  70 + classesObjType: '',
  71 + classesObjId: '',
  72 + classesObjName: '',
  73 + maxLastOffset: '',
  74 + communityId: ''
  75 + },
  76 + rules: {
  77 + classesName: [
  78 + { required: true, message: this.$t('addAttendanceClasses.classesNameRequired'), trigger: 'blur' },
  79 + { max: 64, message: this.$t('addAttendanceClasses.classesNameMaxLength'), trigger: 'blur' }
  80 + ],
  81 + timeOffset: [
  82 + { required: true, message: this.$t('addAttendanceClasses.timeOffsetRequired'), trigger: 'blur' },
  83 + ],
  84 + lateOffset: [
  85 + { required: true, message: this.$t('addAttendanceClasses.lateOffsetRequired'), trigger: 'blur' },
  86 + { max: 20, message: this.$t('addAttendanceClasses.lateOffsetMaxLength'), trigger: 'blur' }
  87 + ],
  88 + leaveOffset: [
  89 + { required: true, message: this.$t('addAttendanceClasses.leaveOffsetRequired'), trigger: 'blur' },
  90 + { max: 20, message: this.$t('addAttendanceClasses.leaveOffsetMaxLength'), trigger: 'blur' }
  91 + ],
  92 + maxLastOffset: [
  93 + { required: true, message: this.$t('addAttendanceClasses.maxLastOffsetRequired'), trigger: 'blur' }
  94 + ]
  95 + }
  96 + }
  97 + },
  98 + methods: {
  99 + open(params) {
  100 + this.formData = {
  101 + ...this.formData,
  102 + ...params,
  103 + communityId: getCommunityId()
  104 + }
  105 + this.visible = true
  106 + },
  107 + closeDialog() {
  108 + this.visible = false
  109 + this.$refs.form.resetFields()
  110 + },
  111 + async saveAttendanceClassesInfo() {
  112 + try {
  113 + await this.$refs.form.validate()
  114 + const response = await saveAttendanceClasses(this.formData)
  115 + if (response.code === 0) {
  116 + this.$message.success(this.$t('addAttendanceClasses.saveSuccess'))
  117 + this.$emit('success')
  118 + this.closeDialog()
  119 + } else {
  120 + this.$message.error(response.msg)
  121 + }
  122 + } catch (error) {
  123 + console.error('保存失败:', error)
  124 + }
  125 + }
  126 + }
  127 +}
  128 +</script>
0 \ No newline at end of file 129 \ No newline at end of file
src/components/oa/chooseOrgTree.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('staffAttendance.chooseOrg')"
  4 + :visible.sync="visible"
  5 + width="60%"
  6 + @close="handleClose"
  7 + >
  8 + <el-tree
  9 + ref="orgTree"
  10 + :data="orgs"
  11 + node-key="id"
  12 + :props="defaultProps"
  13 + :highlight-current="true"
  14 + @node-click="handleNodeClick"
  15 + ></el-tree>
  16 +
  17 + <span slot="footer" class="dialog-footer">
  18 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  19 + <el-button type="primary" @click="handleConfirm">{{ $t('common.confirm') }}</el-button>
  20 + </span>
  21 + </el-dialog>
  22 +</template>
  23 +
  24 +<script>
  25 +import { getCommunityId } from '@/api/community/communityApi'
  26 +import { listOrgTree } from '@/api/oa/staffAttendanceManageApi'
  27 +
  28 +export default {
  29 + name: 'ChooseOrgTree',
  30 + data() {
  31 + return {
  32 + visible: false,
  33 + orgs: [],
  34 + currentOrg: {},
  35 + defaultProps: {
  36 + children: 'children',
  37 + label: 'name'
  38 + }
  39 + }
  40 + },
  41 + methods: {
  42 + open() {
  43 + this.visible = true
  44 + this.loadOrgs()
  45 + },
  46 +
  47 + async loadOrgs() {
  48 + try {
  49 + const params = {
  50 + communityId: getCommunityId()
  51 + }
  52 + const { data } = await listOrgTree(params)
  53 + this.orgs = data || []
  54 + } catch (error) {
  55 + console.error('Failed to load orgs:', error)
  56 + this.$message.error(this.$t('staffAttendance.loadOrgFailed'))
  57 + }
  58 + },
  59 +
  60 + handleNodeClick(data) {
  61 + this.currentOrg = data
  62 + },
  63 +
  64 + handleConfirm() {
  65 + if (!this.currentOrg || !this.currentOrg.id) {
  66 + this.$message.warning(this.$t('staffAttendance.selectOrgFirst'))
  67 + return
  68 + }
  69 +
  70 + this.$emit('switchOrg', {
  71 + orgId: this.currentOrg.id,
  72 + allOrgName: this.getOrgFullName(this.currentOrg)
  73 + })
  74 + this.visible = false
  75 + },
  76 +
  77 + getOrgFullName(node) {
  78 + if (!node) return ''
  79 +
  80 + let names = []
  81 + let currentNode = node
  82 +
  83 + while (currentNode) {
  84 + names.unshift(currentNode.name)
  85 + currentNode = this.$parent ? this.$parent.getNode(currentNode.parentId) : null
  86 + }
  87 +
  88 + return names.join('/')
  89 + },
  90 +
  91 + handleClose() {
  92 + this.currentOrg = {}
  93 + }
  94 + }
  95 +}
  96 +</script>
  97 +
  98 +<style lang="scss" scoped>
  99 +.el-tree {
  100 + max-height: 500px;
  101 + overflow-y: auto;
  102 +}
  103 +
  104 +.dialog-footer {
  105 + text-align: right;
  106 +}
  107 +</style>
0 \ No newline at end of file 108 \ No newline at end of file
src/components/oa/deleteAttendanceClasses.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('deleteAttendanceClasses.title')"
  4 + :visible.sync="visible"
  5 + width="30%"
  6 + @close="closeDialog"
  7 + >
  8 + <div class="confirm-content">
  9 + <p>{{ $t('deleteAttendanceClasses.confirmText') }}</p>
  10 + </div>
  11 + <div slot="footer" class="dialog-footer">
  12 + <el-button @click="closeDialog">{{ $t('deleteAttendanceClasses.cancel') }}</el-button>
  13 + <el-button type="primary" @click="deleteAttendanceClasses">{{ $t('deleteAttendanceClasses.confirm') }}</el-button>
  14 + </div>
  15 + </el-dialog>
  16 +</template>
  17 +
  18 +<script>
  19 +import { deleteAttendanceClasses } from '@/api/oa/attendanceClassesManageApi'
  20 +import { getCommunityId } from '@/api/community/communityApi'
  21 +
  22 +export default {
  23 + name: 'DeleteAttendanceClasses',
  24 + data() {
  25 + return {
  26 + visible: false,
  27 + formData: {
  28 + classesId: '',
  29 + communityId: ''
  30 + }
  31 + }
  32 + },
  33 + methods: {
  34 + open(params) {
  35 + this.formData = {
  36 + classesId: params.classesId,
  37 + communityId: getCommunityId()
  38 + }
  39 + this.visible = true
  40 + },
  41 + closeDialog() {
  42 + this.visible = false
  43 + },
  44 + async deleteAttendanceClasses() {
  45 + try {
  46 + const response = await deleteAttendanceClasses(this.formData)
  47 + if (response.code === 0) {
  48 + this.$message.success(this.$t('deleteAttendanceClasses.deleteSuccess'))
  49 + this.$emit('success')
  50 + this.closeDialog()
  51 + } else {
  52 + this.$message.error(response.msg)
  53 + }
  54 + } catch (error) {
  55 + console.error('删除失败:', error)
  56 + this.$message.error(this.$t('deleteAttendanceClasses.deleteError'))
  57 + }
  58 + }
  59 + }
  60 +}
  61 +</script>
  62 +
  63 +<style scoped>
  64 +.confirm-content {
  65 + text-align: center;
  66 + font-size: 16px;
  67 + padding: 20px 0;
  68 +}
  69 +</style>
0 \ No newline at end of file 70 \ No newline at end of file
src/components/oa/deleteAttendanceClassesStaff.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('attendanceClassesStaffManage.delete.title')"
  4 + :visible.sync="visible"
  5 + width="30%"
  6 + @close="handleClose"
  7 + >
  8 + <div style="text-align: center;">
  9 + <p>{{ $t('attendanceClassesStaffManage.delete.confirmText') }}</p>
  10 + </div>
  11 + <span slot="footer" class="dialog-footer">
  12 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  13 + <el-button type="primary" @click="deleteAttendanceClassesStaff" :loading="loading">
  14 + {{ $t('common.confirm') }}
  15 + </el-button>
  16 + </span>
  17 + </el-dialog>
  18 +</template>
  19 +
  20 +<script>
  21 +import { deleteAttendanceClassesStaff } from '@/api/oa/attendanceClassesStaffManageApi'
  22 +import { getCommunityId } from '@/api/community/communityApi'
  23 +
  24 +export default {
  25 + name: 'DeleteAttendanceClassesStaff',
  26 + data() {
  27 + return {
  28 + visible: false,
  29 + loading: false,
  30 + currentRow: {}
  31 + }
  32 + },
  33 + methods: {
  34 + open(row) {
  35 + this.currentRow = row
  36 + this.visible = true
  37 + },
  38 + async deleteAttendanceClassesStaff() {
  39 + try {
  40 + this.loading = true
  41 + const params = {
  42 + ...this.currentRow,
  43 + communityId: getCommunityId()
  44 + }
  45 + await deleteAttendanceClassesStaff(params)
  46 + this.$message.success(this.$t('attendanceClassesStaffManage.delete.success'))
  47 + this.$emit('success')
  48 + this.visible = false
  49 + } catch (error) {
  50 + console.error('删除考勤员工失败:', error)
  51 + this.$message.error(this.$t('attendanceClassesStaffManage.delete.error'))
  52 + } finally {
  53 + this.loading = false
  54 + }
  55 + },
  56 + handleClose() {
  57 + this.currentRow = {}
  58 + }
  59 + }
  60 +}
  61 +</script>
0 \ No newline at end of file 62 \ No newline at end of file
src/components/oa/editAttendanceClasses.vue 0 → 100644
  1 +<template>
  2 + <el-dialog :title="$t('editAttendanceClasses.title')" :visible.sync="visible" width="60%" @close="closeDialog">
  3 + <el-form ref="form" :model="formData" :rules="rules" label-width="150px" label-position="right">
  4 + <el-row>
  5 + <el-col :span="12">
  6 + <el-form-item :label="$t('editAttendanceClasses.classesName')" prop="classesName">
  7 + <el-input v-model="formData.classesName"
  8 + :placeholder="$t('editAttendanceClasses.classesNamePlaceholder')" />
  9 + </el-form-item>
  10 + </el-col>
  11 + <el-col :span="12">
  12 + <el-form-item :label="$t('editAttendanceClasses.timeOffset')" prop="timeOffset">
  13 + <el-input v-model="formData.timeOffset" type="number"
  14 + :placeholder="$t('editAttendanceClasses.timeOffsetPlaceholder')">
  15 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  16 + </el-input>
  17 + </el-form-item>
  18 + </el-col>
  19 + </el-row>
  20 + <el-row>
  21 + <el-col :span="12">
  22 + <el-form-item :label="$t('editAttendanceClasses.lateOffset')" prop="lateOffset">
  23 + <el-input v-model="formData.lateOffset" type="number"
  24 + :placeholder="$t('editAttendanceClasses.lateOffsetPlaceholder')">
  25 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  26 + </el-input>
  27 + </el-form-item>
  28 + </el-col>
  29 + <el-col :span="12">
  30 + <el-form-item :label="$t('editAttendanceClasses.leaveOffset')" prop="leaveOffset">
  31 + <el-input v-model="formData.leaveOffset" type="number"
  32 + :placeholder="$t('editAttendanceClasses.leaveOffsetPlaceholder')">
  33 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  34 + </el-input>
  35 + </el-form-item>
  36 + </el-col>
  37 + </el-row>
  38 + <el-row>
  39 + <el-col :span="12">
  40 + <el-form-item :label="$t('editAttendanceClasses.maxLastOffset')" prop="maxLastOffset">
  41 + <el-input v-model="formData.maxLastOffset" type="number"
  42 + :placeholder="$t('editAttendanceClasses.maxLastOffsetPlaceholder')">
  43 + <template slot="append">{{ $t('attendanceClassesManage.minute') }}</template>
  44 + </el-input>
  45 + </el-form-item>
  46 + </el-col>
  47 + </el-row>
  48 + </el-form>
  49 + <div slot="footer" class="dialog-footer">
  50 + <el-button @click="closeDialog">{{ $t('common.cancel') }}</el-button>
  51 + <el-button type="primary" @click="editAttendanceClasses">{{ $t('common.save') }}</el-button>
  52 + </div>
  53 + </el-dialog>
  54 +</template>
  55 +
  56 +<script>
  57 +import { updateAttendanceClasses } from '@/api/oa/attendanceClassesManageApi'
  58 +import { getCommunityId } from '@/api/community/communityApi'
  59 +import { getDict } from '@/api/community/communityApi'
  60 +
  61 +export default {
  62 + name: 'EditAttendanceClasses',
  63 + data() {
  64 + return {
  65 + visible: false,
  66 + formData: {
  67 + classesId: '',
  68 + classesName: '',
  69 + timeOffset: '',
  70 + clockCount: '',
  71 + clockTypes: [],
  72 + clockType: '',
  73 + clockTypeValue: '',
  74 + leaveOffset: '',
  75 + lateOffset: '',
  76 + maxLastOffset: '',
  77 + attrs: [],
  78 + clockTypeValues: [],
  79 + communityId: ''
  80 + },
  81 + rules: {
  82 + classesName: [
  83 + { required: true, message: this.$t('editAttendanceClasses.classesNameRequired'), trigger: 'blur' },
  84 + { max: 64, message: this.$t('editAttendanceClasses.classesNameMaxLength'), trigger: 'blur' }
  85 + ],
  86 + timeOffset: [
  87 + { required: true, message: this.$t('editAttendanceClasses.timeOffsetRequired'), trigger: 'blur' },
  88 + ],
  89 + lateOffset: [
  90 + { required: true, message: this.$t('editAttendanceClasses.lateOffsetRequired'), trigger: 'blur' },
  91 + { max: 20, message: this.$t('editAttendanceClasses.lateOffsetMaxLength'), trigger: 'blur' }
  92 + ],
  93 + leaveOffset: [
  94 + { required: true, message: this.$t('editAttendanceClasses.leaveOffsetRequired'), trigger: 'blur' },
  95 + { max: 20, message: this.$t('editAttendanceClasses.leaveOffsetMaxLength'), trigger: 'blur' }
  96 + ],
  97 + maxLastOffset: [
  98 + { required: true, message: this.$t('editAttendanceClasses.maxLastOffsetRequired'), trigger: 'blur' }
  99 + ],
  100 + classesId: [
  101 + { required: true, message: this.$t('editAttendanceClasses.classesIdRequired'), trigger: 'blur' }
  102 + ]
  103 + }
  104 + }
  105 + },
  106 + methods: {
  107 + open(params) {
  108 + this.formData = {
  109 + ...this.formData,
  110 + ...params,
  111 + communityId: getCommunityId()
  112 + }
  113 + this.getClockTypes()
  114 + this.visible = true
  115 + },
  116 + closeDialog() {
  117 + this.visible = false
  118 + this.$refs.form.resetFields()
  119 + },
  120 + async getClockTypes() {
  121 + try {
  122 + const data = await getDict('attendance_classes', 'clock_type')
  123 + this.formData.clockTypes = data
  124 + } catch (error) {
  125 + console.error('获取字典数据失败:', error)
  126 + }
  127 + },
  128 + async editAttendanceClasses() {
  129 + try {
  130 + await this.$refs.form.validate()
  131 + const response = await updateAttendanceClasses(this.formData)
  132 + if (response.code === 0) {
  133 + this.$message.success(this.$t('editAttendanceClasses.editSuccess'))
  134 + this.$emit('success')
  135 + this.closeDialog()
  136 + } else {
  137 + this.$message.error(response.msg)
  138 + }
  139 + } catch (error) {
  140 + console.error('修改失败:', error)
  141 + }
  142 + }
  143 + }
  144 +}
  145 +</script>
0 \ No newline at end of file 146 \ No newline at end of file
src/components/oa/orgTreeShow.vue
1 <template> 1 <template>
2 - <div class="org-tree-show-container"> 2 + <div class="org-tree-container">
3 <el-tree 3 <el-tree
4 ref="orgTree" 4 ref="orgTree"
5 - :data="orgTreeShowInfo.orgs" 5 + :data="orgData"
6 :props="defaultProps" 6 :props="defaultProps"
7 node-key="id" 7 node-key="id"
8 default-expand-all 8 default-expand-all
9 highlight-current 9 highlight-current
10 - @node-click="handleNodeClick">  
11 - </el-tree> 10 + :expand-on-click-node="false"
  11 + @node-click="handleNodeClick"
  12 + ></el-tree>
12 </div> 13 </div>
13 </template> 14 </template>
14 15
15 <script> 16 <script>
16 -import { listOrgTree } from '@/api/oa/addExamineStaffApi' 17 +import { listOrgTree } from '@/api/oa/addAttendanceClassesStaffApi'
17 import { getCommunityId } from '@/api/community/communityApi' 18 import { getCommunityId } from '@/api/community/communityApi'
18 19
19 export default { 20 export default {
20 name: 'OrgTreeShow', 21 name: 'OrgTreeShow',
21 - props: {  
22 - callBackListener: {  
23 - type: String,  
24 - default: ''  
25 - }  
26 - },  
27 data() { 22 data() {
28 return { 23 return {
29 - orgTreeShowInfo: {  
30 - orgs: [],  
31 - orgId: '',  
32 - curOrg: {}  
33 - }, 24 + orgData: [],
34 defaultProps: { 25 defaultProps: {
35 children: 'children', 26 children: 'children',
36 label: 'text' 27 label: 'text'
37 - } 28 + },
  29 + communityId: getCommunityId()
38 } 30 }
39 }, 31 },
40 - mounted() {  
41 - this._loadOrgsShow()  
42 - },  
43 methods: { 32 methods: {
44 - refreshTree() {  
45 - this._loadOrgsShow()  
46 - },  
47 - async _loadOrgsShow() { 33 + async refreshTree() {
48 try { 34 try {
49 const params = { 35 const params = {
50 - communityId: getCommunityId() 36 + communityId: this.communityId
51 } 37 }
52 const res = await listOrgTree(params) 38 const res = await listOrgTree(params)
53 - this.orgTreeShowInfo.orgs = res.data 39 + this.orgData = res.data
54 } catch (error) { 40 } catch (error) {
55 - console.error('加载组织树失败:', error) 41 + this.$message.error(this.$t('orgTree.loadFailed'))
56 } 42 }
57 }, 43 },
58 handleNodeClick(data) { 44 handleNodeClick(data) {
59 - this.orgTreeShowInfo.curOrg = data  
60 - this.orgTreeShowInfo.curOrg.orgId = data.id  
61 - this.$emit(this.callBackListener, 'switchOrg', { 45 + this.$emit('switchOrg', {
62 orgId: data.id, 46 orgId: data.id,
63 orgName: data.text 47 orgName: data.text
64 }) 48 })
65 } 49 }
  50 + },
  51 + mounted() {
  52 + this.refreshTree()
66 } 53 }
67 } 54 }
68 </script> 55 </script>
69 56
70 <style lang="scss" scoped> 57 <style lang="scss" scoped>
71 -.org-tree-show-container {  
72 - height: 100%;  
73 -  
74 - /deep/ .el-tree {  
75 - height: 100%;  
76 - overflow-y: auto; 58 +.org-tree-container {
  59 + height: 500px;
  60 + overflow-y: auto;
  61 + padding: 10px;
  62 +
  63 + ::v-deep .el-tree {
  64 + background-color: transparent;
77 65
78 .el-tree-node__content { 66 .el-tree-node__content {
79 height: 36px; 67 height: 36px;
80 - line-height: 36px; 68 +
  69 + &:hover {
  70 + background-color: #f5f7fa;
  71 + }
81 } 72 }
82 73
83 - .el-tree-node.is-current > .el-tree-node__content {  
84 - background-color: #f0f7ff; 74 + .is-current > .el-tree-node__content {
  75 + background-color: #ecf5ff;
85 } 76 }
86 } 77 }
87 } 78 }
src/components/oa/selectStaff.vue
1 <template> 1 <template>
2 - <el-dialog  
3 - :title="$t('selectStaff.title')"  
4 - :visible.sync="dialogVisible" 2 + <el-dialog
  3 + :title="$t('selectStaff.title')"
  4 + :visible.sync="visible"
5 width="70%" 5 width="70%"
6 - :close-on-click-modal="false">  
7 - 6 + :close-on-click-modal="false"
  7 + >
8 <el-row :gutter="20"> 8 <el-row :gutter="20">
9 <el-col :span="12" class="border-right"> 9 <el-col :span="12" class="border-right">
10 - <div class="text-center section-title">  
11 - {{ $t('selectStaff.orgInfo') }}  
12 - </div>  
13 - <div class="org-tree-container">  
14 - <org-tree-show  
15 - ref="orgTree"  
16 - :call-back-listener="'selectStaff'">  
17 - </org-tree-show> 10 + <div class="text-center mb-20">
  11 + <h4>{{ $t('selectStaff.orgInfo') }}</h4>
18 </div> 12 </div>
  13 + <org-tree-show
  14 + ref="orgTree"
  15 + @switchOrg="handleSwitchOrg"
  16 + ></org-tree-show>
19 </el-col> 17 </el-col>
20 - 18 +
21 <el-col :span="12"> 19 <el-col :span="12">
22 - <div class="text-center section-title">  
23 - {{ $t('selectStaff.staffInfo') }} 20 + <div class="text-center mb-20">
  21 + <h4>{{ $t('selectStaff.staffInfo') }}</h4>
24 </div> 22 </div>
25 - <div class="staff-list-container"> 23 + <div class="staff-list">
26 <div 24 <div
27 - v-for="(item, index) in selectStaffInfo.staffs" 25 + v-for="(staff, index) in staffList"
28 :key="index" 26 :key="index"
29 class="staff-item" 27 class="staff-item"
30 - :class="{ 'selected': selectStaffInfo.curStaffId === item.staffId }"  
31 - @click="_changeStaff(item)"> 28 + :class="{ 'selected': currentStaffId === staff.staffId }"
  29 + @click="handleSelectStaff(staff)"
  30 + >
32 <div> 31 <div>
33 - <i class="el-icon-user margin-right-xs"></i>  
34 - {{ item.name }} 32 + <i class="el-icon-user"></i>
  33 + {{ staff.name }}
35 </div> 34 </div>
36 - <div>{{ item.tel }}</div> 35 + <div>{{ staff.tel }}</div>
37 </div> 36 </div>
38 </div> 37 </div>
39 </el-col> 38 </el-col>
40 </el-row> 39 </el-row>
41 - 40 +
42 <div 41 <div
43 - v-if="selectStaffInfo.staff.from === 'bpmn' ||  
44 - selectStaffInfo.staff.from === 'purchase' ||  
45 - selectStaffInfo.staff.from === 'contract'"  
46 - class="dialog-footer text-right">  
47 - <el-button size="small" @click="_firstUser">  
48 - {{ $t('selectStaff.submitter') }}  
49 - </el-button>  
50 - <el-button size="small" @click="_customUser">  
51 - {{ $t('selectStaff.dynamicAssign') }}  
52 - </el-button> 42 + v-if="staffData.from === 'bpmn' || staffData.from === 'purchase' || staffData.from === 'contract'"
  43 + slot="footer"
  44 + class="dialog-footer"
  45 + >
  46 + <el-button @click="handleFirstUser">{{ $t('selectStaff.submitter') }}</el-button>
  47 + <el-button @click="handleCustomUser">{{ $t('selectStaff.dynamicAssign') }}</el-button>
53 </div> 48 </div>
54 </el-dialog> 49 </el-dialog>
55 </template> 50 </template>
56 51
57 <script> 52 <script>
58 -import { queryStaffInfos } from '@/api/oa/addExamineStaffApi'  
59 -import OrgTreeShow from '@/components/oa/OrgTreeShow' 53 +import OrgTreeShow from './OrgTreeShow'
  54 +import { queryStaffInfos } from '@/api/oa/addAttendanceClassesStaffApi'
60 55
61 export default { 56 export default {
62 name: 'SelectStaff', 57 name: 'SelectStaff',
@@ -65,25 +60,23 @@ export default { @@ -65,25 +60,23 @@ export default {
65 }, 60 },
66 data() { 61 data() {
67 return { 62 return {
68 - dialogVisible: false,  
69 - selectStaffInfo: {  
70 - staffs: [],  
71 - curStaffId: '',  
72 - curStaffName: '',  
73 - staff: {}  
74 - } 63 + visible: false,
  64 + staffData: {},
  65 + staffList: [],
  66 + currentStaffId: '',
  67 + currentOrgId: ''
75 } 68 }
76 }, 69 },
77 - mounted() {  
78 - this.$on('selectStaff', 'switchOrg', this.loadStaff)  
79 - },  
80 methods: { 70 methods: {
81 open(staff) { 71 open(staff) {
82 - this.selectStaffInfo.staff = staff  
83 - this.dialogVisible = true  
84 - this.$refs.orgTree.refreshTree() 72 + this.staffData = staff
  73 + this.visible = true
  74 + this.$nextTick(() => {
  75 + this.$refs.orgTree.refreshTree()
  76 + })
85 }, 77 },
86 - async loadStaff(org) { 78 + async handleSwitchOrg(org) {
  79 + this.currentOrgId = org.orgId
87 try { 80 try {
88 const params = { 81 const params = {
89 page: 1, 82 page: 1,
@@ -91,41 +84,35 @@ export default { @@ -91,41 +84,35 @@ export default {
91 orgId: org.orgId 84 orgId: org.orgId
92 } 85 }
93 const res = await queryStaffInfos(params) 86 const res = await queryStaffInfos(params)
94 - this.selectStaffInfo.staffs = res.staffs  
95 - if (res.staffs.length > 0) {  
96 - this.selectStaffInfo.curStaffId = res.staffs[0].orgId 87 + this.staffList = res.data.staffs
  88 + if (this.staffList.length > 0) {
  89 + this.currentStaffId = this.staffList[0].staffId
97 } 90 }
98 } catch (error) { 91 } catch (error) {
99 - console.error('加载员工失败:', error) 92 + this.$message.error(this.$t('selectStaff.loadStaffFailed'))
100 } 93 }
101 }, 94 },
102 - _changeStaff(item) {  
103 - this.selectStaffInfo.staff.staffId = item.userId  
104 - this.selectStaffInfo.staff.staffName = item.userName  
105 - this.selectStaffInfo.staff.staffTel = item.tel  
106 - this.dialogVisible = false  
107 -  
108 - if (typeof this.selectStaffInfo.staff.call === 'function') {  
109 - this.selectStaffInfo.staff.call(this.selectStaffInfo.staff)  
110 - } 95 + handleSelectStaff(staff) {
  96 + this.$emit('change', {
  97 + staffId: staff.userId,
  98 + staffName: staff.userName,
  99 + staffTel: staff.tel
  100 + })
  101 + this.visible = false
111 }, 102 },
112 - _firstUser() {  
113 - this.selectStaffInfo.staff.staffId = '${startUserId}'  
114 - this.selectStaffInfo.staff.staffName = this.$t('selectStaff.submitter')  
115 - this.dialogVisible = false  
116 -  
117 - if (typeof this.selectStaffInfo.staff.call === 'function') {  
118 - this.selectStaffInfo.staff.call(this.selectStaffInfo.staff)  
119 - } 103 + handleFirstUser() {
  104 + this.$emit('change', {
  105 + staffId: '${startUserId}',
  106 + staffName: this.$t('selectStaff.submitter')
  107 + })
  108 + this.visible = false
120 }, 109 },
121 - _customUser() {  
122 - this.selectStaffInfo.staff.staffId = '${nextUserId}'  
123 - this.selectStaffInfo.staff.staffName = this.$t('selectStaff.dynamicAssign')  
124 - this.dialogVisible = false  
125 -  
126 - if (typeof this.selectStaffInfo.staff.call === 'function') {  
127 - this.selectStaffInfo.staff.call(this.selectStaffInfo.staff)  
128 - } 110 + handleCustomUser() {
  111 + this.$emit('change', {
  112 + staffId: '${nextUserId}',
  113 + staffName: this.$t('selectStaff.dynamicAssign')
  114 + })
  115 + this.visible = false
129 } 116 }
130 } 117 }
131 } 118 }
@@ -136,22 +123,17 @@ export default { @@ -136,22 +123,17 @@ export default {
136 border-right: 1px solid #ebeef5; 123 border-right: 1px solid #ebeef5;
137 } 124 }
138 125
139 -.section-title {  
140 - font-weight: bold;  
141 - margin-bottom: 15px;  
142 - font-size: 16px; 126 +.text-center {
  127 + text-align: center;
143 } 128 }
144 129
145 -.org-tree-container {  
146 - height: 400px;  
147 - overflow-y: auto;  
148 - padding: 10px; 130 +.mb-20 {
  131 + margin-bottom: 20px;
149 } 132 }
150 133
151 -.staff-list-container {  
152 - height: 400px; 134 +.staff-list {
  135 + max-height: 500px;
153 overflow-y: auto; 136 overflow-y: auto;
154 - padding: 10px;  
155 } 137 }
156 138
157 .staff-item { 139 .staff-item {
@@ -160,30 +142,14 @@ export default { @@ -160,30 +142,14 @@ export default {
160 border: 1px solid #ebeef5; 142 border: 1px solid #ebeef5;
161 border-radius: 4px; 143 border-radius: 4px;
162 cursor: pointer; 144 cursor: pointer;
163 - 145 +
164 &:hover { 146 &:hover {
165 background-color: #f5f7fa; 147 background-color: #f5f7fa;
166 } 148 }
167 - 149 +
168 &.selected { 150 &.selected {
169 background-color: #ecf5ff; 151 background-color: #ecf5ff;
170 border-color: #d9ecff; 152 border-color: #d9ecff;
171 } 153 }
172 } 154 }
173 -  
174 -.dialog-footer {  
175 - margin-top: 20px;  
176 -}  
177 -  
178 -.margin-right-xs {  
179 - margin-right: 5px;  
180 -}  
181 -  
182 -.text-center {  
183 - text-align: center;  
184 -}  
185 -  
186 -.text-right {  
187 - text-align: right;  
188 -}  
189 </style> 155 </style>
190 \ No newline at end of file 156 \ No newline at end of file
src/components/oa/staffAttendanceDetail.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('staffAttendance.attendanceDetail')"
  4 + :visible.sync="visible"
  5 + width="70%"
  6 + >
  7 + <el-table
  8 + :data="details"
  9 + border
  10 + style="width: 100%"
  11 + v-loading="loading"
  12 + >
  13 + <el-table-column
  14 + prop="staffName"
  15 + :label="$t('staffAttendance.staffName')"
  16 + align="center"
  17 + ></el-table-column>
  18 +
  19 + <el-table-column
  20 + :label="$t('staffAttendance.face')"
  21 + align="center"
  22 + >
  23 + <template slot-scope="scope">
  24 + <el-image
  25 + style="width: 60px; height: 60px; border-radius: 4px;"
  26 + :src="scope.row.facePath || '/img/noPhoto.jpg'"
  27 + fit="cover"
  28 + ></el-image>
  29 + </template>
  30 + </el-table-column>
  31 +
  32 + <el-table-column
  33 + prop="clockTime"
  34 + :label="$t('staffAttendance.clockTime')"
  35 + align="center"
  36 + ></el-table-column>
  37 + </el-table>
  38 +
  39 + <span slot="footer" class="dialog-footer">
  40 + <el-button @click="visible = false">{{ $t('common.close') }}</el-button>
  41 + </span>
  42 + </el-dialog>
  43 +</template>
  44 +
  45 +<script>
  46 +import { getCommunityId } from '@/api/community/communityApi'
  47 +import { queryAttendanceLog } from '@/api/oa/staffAttendanceManageApi'
  48 +
  49 +export default {
  50 + name: 'StaffAttendanceDetail',
  51 + data() {
  52 + return {
  53 + visible: false,
  54 + loading: false,
  55 + details: [],
  56 + params: {}
  57 + }
  58 + },
  59 + methods: {
  60 + open(params) {
  61 + this.params = params || {}
  62 + this.visible = true
  63 + this.loadDetails()
  64 + },
  65 +
  66 + async loadDetails() {
  67 + try {
  68 + this.loading = true
  69 + const { data } = await queryAttendanceLog({
  70 + ...this.params,
  71 + page: 1,
  72 + row: 30,
  73 + communityId: getCommunityId()
  74 + })
  75 + this.details = data || []
  76 + } catch (error) {
  77 + console.error('Failed to load attendance details:', error)
  78 + this.$message.error(this.$t('staffAttendance.loadDetailFailed'))
  79 + } finally {
  80 + this.loading = false
  81 + }
  82 + }
  83 + }
  84 +}
  85 +</script>
  86 +
  87 +<style lang="scss" scoped>
  88 +.dialog-footer {
  89 + text-align: right;
  90 +}
  91 +</style>
0 \ No newline at end of file 92 \ No newline at end of file
src/components/oa/staffAttendanceReplenishCheckIn.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('staffAttendance.replenishCheckIn')"
  4 + :visible.sync="visible"
  5 + width="50%"
  6 + >
  7 + <el-form label-position="left" label-width="120px">
  8 + <el-form-item :label="$t('staffAttendance.attendanceTask')" required>
  9 + <el-select
  10 + v-model="form.detailId"
  11 + :placeholder="$t('staffAttendance.selectTask')"
  12 + style="width:100%"
  13 + >
  14 + <el-option
  15 + v-for="(item,index) in details"
  16 + :key="index"
  17 + :label="`${item.specName}(${item.value})`"
  18 + :value="item.detailId"
  19 + ></el-option>
  20 + </el-select>
  21 + </el-form-item>
  22 +
  23 + <el-form-item :label="$t('staffAttendance.reason')" required>
  24 + <el-input
  25 + type="textarea"
  26 + :rows="4"
  27 + v-model="form.remark"
  28 + :placeholder="$t('staffAttendance.inputReason')"
  29 + ></el-input>
  30 + </el-form-item>
  31 + </el-form>
  32 +
  33 + <span slot="footer" class="dialog-footer">
  34 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  35 + <el-button type="primary" @click="handleSubmit">{{ $t('staffAttendance.submitReplenish') }}</el-button>
  36 + </span>
  37 + </el-dialog>
  38 +</template>
  39 +
  40 +<script>
  41 +import { attendanceReplenishCheckIn } from '@/api/oa/staffAttendanceManageApi'
  42 +
  43 +export default {
  44 + name: 'StaffAttendanceReplenishCheckIn',
  45 + data() {
  46 + return {
  47 + visible: false,
  48 + details: [],
  49 + form: {
  50 + detailId: '',
  51 + remark: ''
  52 + }
  53 + }
  54 + },
  55 + methods: {
  56 + open(details) {
  57 + this.details = details || []
  58 + this.form = {
  59 + detailId: '',
  60 + remark: ''
  61 + }
  62 + this.visible = true
  63 + },
  64 +
  65 + async handleSubmit() {
  66 + if (!this.form.detailId) {
  67 + this.$message.warning(this.$t('staffAttendance.selectTaskFirst'))
  68 + return
  69 + }
  70 +
  71 + if (!this.form.remark) {
  72 + this.$message.warning(this.$t('staffAttendance.inputReasonFirst'))
  73 + return
  74 + }
  75 +
  76 + try {
  77 + await attendanceReplenishCheckIn(this.form)
  78 + this.$message.success(this.$t('staffAttendance.replenishSuccess'))
  79 + this.$emit('success')
  80 + this.visible = false
  81 + } catch (error) {
  82 + console.error('Failed to submit replenish:', error)
  83 + this.$message.error(this.$t('staffAttendance.replenishFailed'))
  84 + }
  85 + }
  86 + }
  87 +}
  88 +</script>
  89 +
  90 +<style lang="scss" scoped>
  91 +.dialog-footer {
  92 + text-align: right;
  93 +}
  94 +</style>
0 \ No newline at end of file 95 \ No newline at end of file
src/components/oa/todayAttendanceDetail.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('todayAttendanceDetail.title')"
  4 + :visible.sync="visible"
  5 + width="80%"
  6 + @close="handleClose"
  7 + >
  8 + <el-table
  9 + :data="tableData"
  10 + border
  11 + style="width: 100%"
  12 + >
  13 + <el-table-column
  14 + prop="specCd"
  15 + :label="$t('todayAttendanceDetail.table.name')"
  16 + align="center"
  17 + >
  18 + <template slot-scope="scope">
  19 + {{ scope.row.specCd === '1001' ? $t('todayAttendanceDetail.table.work') : $t('todayAttendanceDetail.table.offWork') }}
  20 + </template>
  21 + </el-table-column>
  22 + <el-table-column
  23 + :label="$t('todayAttendanceDetail.table.attendanceRange')"
  24 + align="center"
  25 + >
  26 + <template slot-scope="scope">
  27 + {{ scope.row.specCd === '1001'
  28 + ? `${timeMinFormat(scope.row.leaveValue)}~${timeMinFormat(scope.row.value)}`
  29 + : `${timeMinFormat(scope.row.value)}~${timeMinFormat(scope.row.lateValue)}` }}
  30 + </template>
  31 + </el-table-column>
  32 + <el-table-column
  33 + :label="$t('todayAttendanceDetail.table.lateEarly')"
  34 + align="center"
  35 + >
  36 + <template slot-scope="scope">
  37 + {{ scope.row.specCd === '1001'
  38 + ? `${timeMinFormat(scope.row.value)}~${timeMinFormat(scope.row.lateValue)}`
  39 + : `${timeMinFormat(scope.row.leaveValue)}~${timeMinFormat(scope.row.value)}` }}
  40 + </template>
  41 + </el-table-column>
  42 + <el-table-column
  43 + prop="stateName"
  44 + :label="$t('todayAttendanceDetail.table.status')"
  45 + align="center"
  46 + />
  47 + <el-table-column
  48 + :label="$t('todayAttendanceDetail.table.attendanceTime')"
  49 + align="center"
  50 + >
  51 + <template slot-scope="scope">
  52 + {{ scope.row.checkTime || $t('todayAttendanceDetail.table.notCheckIn') }}
  53 + </template>
  54 + </el-table-column>
  55 + <el-table-column
  56 + :label="$t('todayAttendanceDetail.table.snapshot')"
  57 + align="center"
  58 + >
  59 + <template slot-scope="scope">
  60 + <el-image
  61 + v-if="scope.row.state !== '10000'"
  62 + style="width: 60px; height: 60px;"
  63 + :src="scope.row.facePath"
  64 + :preview-src-list="[scope.row.facePath]"
  65 + fit="cover"
  66 + />
  67 + </template>
  68 + </el-table-column>
  69 + </el-table>
  70 + </el-dialog>
  71 +</template>
  72 +
  73 +<script>
  74 +export default {
  75 + name: 'TodayAttendanceDetail',
  76 + data() {
  77 + return {
  78 + visible: false,
  79 + tableData: []
  80 + }
  81 + },
  82 + methods: {
  83 + open(data) {
  84 + this.tableData = data.attendanceClassesTaskDetails || []
  85 + this.visible = true
  86 + },
  87 + handleClose() {
  88 + this.tableData = []
  89 + this.visible = false
  90 + },
  91 + timeMinFormat(time) {
  92 + if (!time) return ''
  93 + const hours = Math.floor(time / 60)
  94 + const minutes = time % 60
  95 + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
  96 + }
  97 + }
  98 +}
  99 +</script>
  100 +
  101 +<style scoped>
  102 +.el-image {
  103 + cursor: pointer;
  104 + transition: transform 0.3s;
  105 +}
  106 +
  107 +.el-image:hover {
  108 + transform: scale(1.1);
  109 +}
  110 +</style>
0 \ No newline at end of file 111 \ No newline at end of file
src/components/oa/uploadImageUrl.vue
@@ -3,34 +3,36 @@ @@ -3,34 +3,36 @@
3 <div v-for="(image, index) in photos" :key="index" class="image-item"> 3 <div v-for="(image, index) in photos" :key="index" class="image-item">
4 <el-image 4 <el-image
5 :src="image" 5 :src="image"
6 - fit="cover"  
7 - style="width: 100px; height: 100px">  
8 - </el-image> 6 + fit="cover"
  7 + style="width: 100px; height: 100px;"
  8 + :preview-src-list="[image]"
  9 + ></el-image>
9 <i 10 <i
10 - class="el-icon-close remove-icon"  
11 - @click="_removeImage(image)">  
12 - </i> 11 + class="el-icon-delete remove-icon"
  12 + @click="handleRemoveImage(image)"
  13 + ></i>
13 </div> 14 </div>
14 - 15 +
15 <div 16 <div
16 - v-if="photos.length < imageCount"  
17 - class="upload-button"  
18 - @click="_uploadPhoto"> 17 + v-if="photos.length < imageCount"
  18 + class="upload-button"
  19 + @click="triggerUpload"
  20 + >
19 <i class="el-icon-plus"></i> 21 <i class="el-icon-plus"></i>
20 </div> 22 </div>
21 - 23 +
22 <input 24 <input
23 type="file" 25 type="file"
24 - class="file-input" 26 + ref="fileInput"
25 accept="image/*" 27 accept="image/*"
26 - ref="fileInput"  
27 - hidden  
28 - @change="_choosePhoto"> 28 + hidden
  29 + @change="handleFileChange"
  30 + >
29 </div> 31 </div>
30 </template> 32 </template>
31 33
32 <script> 34 <script>
33 -import { uploadImage } from '@/api/oa/editExamineStaffApi' 35 +import { uploadFile } from '@/api/oa/addAttendanceClassesStaffApi'
34 import { getCommunityId } from '@/api/community/communityApi' 36 import { getCommunityId } from '@/api/community/communityApi'
35 37
36 export default { 38 export default {
@@ -38,121 +40,74 @@ export default { @@ -38,121 +40,74 @@ export default {
38 props: { 40 props: {
39 imageCount: { 41 imageCount: {
40 type: Number, 42 type: Number,
41 - default: 99 43 + default: 1
42 } 44 }
43 }, 45 },
44 data() { 46 data() {
45 return { 47 return {
46 - photos: [], // 显示图片数组(base64格式)  
47 - photosUrl: [], // 向父组件传递的数组({fileId, url})  
48 - communityId: '' 48 + photos: [],
  49 + photosUrl: [],
  50 + communityId: getCommunityId()
49 } 51 }
50 }, 52 },
51 - created() {  
52 - this.communityId = getCommunityId()  
53 - },  
54 methods: { 53 methods: {
55 - open(photos) {  
56 - this.photos = []  
57 - this.photosUrl = []  
58 -  
59 - if (!photos || photos.length === 0) return  
60 -  
61 - photos.forEach(photo => {  
62 - if (photo.indexOf('base64,') > -1) {  
63 - this.photos.push(photo)  
64 - return  
65 - }  
66 -  
67 - if (photo.indexOf("https") > -1 || photo.indexOf("http") > -1) {  
68 - this.urlToBase64(photo)  
69 - const urlParams = this._getUrlParams(photo)  
70 - if (urlParams.fileId) {  
71 - this.photosUrl.push({fileId: urlParams.fileId, url: photo})  
72 - }  
73 - return  
74 - }  
75 -  
76 - const url = `/callComponent/download/getFile/file?fileId=${photo}&communityId=-1&time=${new Date()}`  
77 - this.urlToBase64(url)  
78 - this.photosUrl.push({fileId: photo, url})  
79 - })  
80 - },  
81 - clear() {  
82 - this.photos = []  
83 - this.photosUrl = []  
84 - },  
85 - _uploadPhoto() { 54 + triggerUpload() {
86 this.$refs.fileInput.click() 55 this.$refs.fileInput.click()
87 }, 56 },
88 - _choosePhoto(event) {  
89 - const files = event.target.files  
90 - if (!files || files.length === 0) return  
91 -  
92 - const file = files[0] 57 + async handleFileChange(event) {
  58 + const file = event.target.files[0]
  59 + if (!file) return
  60 +
93 if (file.size > 2 * 1024 * 1024) { 61 if (file.size > 2 * 1024 * 1024) {
94 this.$message.error(this.$t('uploadImage.imageSizeLimit')) 62 this.$message.error(this.$t('uploadImage.imageSizeLimit'))
95 return 63 return
96 } 64 }
97 - 65 +
  66 + // 预览图片
98 const reader = new FileReader() 67 const reader = new FileReader()
99 reader.onload = (e) => { 68 reader.onload = (e) => {
100 this.photos.push(e.target.result) 69 this.photos.push(e.target.result)
101 - this._doUploadImageUrl(file)  
102 } 70 }
103 reader.readAsDataURL(file) 71 reader.readAsDataURL(file)
104 -  
105 - event.target.value = null  
106 - },  
107 - _removeImage(image) {  
108 - const index = this.photos.indexOf(image)  
109 - this.photos.splice(index, 1)  
110 - this.photosUrl.splice(index, 1)  
111 - this.$emit('notifyUploadImage', this.photosUrl)  
112 - },  
113 - async _doUploadImageUrl(file) { 72 +
  73 + // 上传图片
114 try { 74 try {
115 const formData = new FormData() 75 const formData = new FormData()
116 - formData.append("uploadFile", file) 76 + formData.append('uploadFile', file)
117 formData.append('communityId', this.communityId) 77 formData.append('communityId', this.communityId)
118 -  
119 - const data = await uploadImage(formData)  
120 - this.photosUrl.push(data) 78 +
  79 + const res = await uploadFile(formData)
  80 + this.photosUrl.push(res.data)
121 this.$emit('notifyUploadImage', this.photosUrl) 81 this.$emit('notifyUploadImage', this.photosUrl)
122 } catch (error) { 82 } catch (error) {
123 - this.$message.error(this.$t('uploadImage.uploadError')) 83 + this.$message.error(this.$t('uploadImage.uploadFailed'))
124 } 84 }
  85 +
  86 + event.target.value = null
125 }, 87 },
126 - _getUrlParams(url) {  
127 - if (url.indexOf('?') < 0) {  
128 - return { fileId: url }  
129 - }  
130 -  
131 - const urlStr = url.split('?')[1]  
132 - const obj = {}  
133 - const paramsArr = urlStr.split('&')  
134 -  
135 - for (let i = 0; i < paramsArr.length; i++) {  
136 - const arr = paramsArr[i].split('=')  
137 - obj[arr[0]] = arr[1] 88 + handleRemoveImage(image) {
  89 + const index = this.photos.indexOf(image)
  90 + if (index > -1) {
  91 + this.photos.splice(index, 1)
  92 + this.photosUrl.splice(index, 1)
  93 + this.$emit('notifyUploadImage', this.photosUrl)
138 } 94 }
139 -  
140 - return obj  
141 }, 95 },
142 - urlToBase64(url) {  
143 - const image = new Image()  
144 - image.setAttribute('crossOrigin', 'anonymous')  
145 - image.src = url  
146 -  
147 - image.onload = () => {  
148 - const canvas = document.createElement('canvas')  
149 - canvas.width = image.width  
150 - canvas.height = image.height  
151 - const ctx = canvas.getContext('2d')  
152 - ctx.drawImage(image, 0, 0, image.width, image.height)  
153 - const base64 = canvas.toDataURL('image/png')  
154 - this.photos.push(base64)  
155 - } 96 + clearImages() {
  97 + this.photos = []
  98 + this.photosUrl = []
  99 + },
  100 + setImages(images) {
  101 + this.clearImages()
  102 + images.forEach(img => {
  103 + if (img.indexOf('base64,') > -1) {
  104 + this.photos.push(img)
  105 + return
  106 + }
  107 + // 处理其他类型的图片URL
  108 + this.photos.push(img)
  109 + this.photosUrl.push({ url: img })
  110 + })
156 } 111 }
157 } 112 }
158 } 113 }
@@ -163,41 +118,39 @@ export default { @@ -163,41 +118,39 @@ export default {
163 display: flex; 118 display: flex;
164 flex-wrap: wrap; 119 flex-wrap: wrap;
165 gap: 10px; 120 gap: 10px;
166 - 121 +
167 .image-item { 122 .image-item {
168 position: relative; 123 position: relative;
169 -  
170 - .remove-icon {  
171 - position: absolute;  
172 - top: 5px;  
173 - right: 5px;  
174 - color: #f56c6c;  
175 - font-size: 16px;  
176 - cursor: pointer;  
177 - background: #fff;  
178 - border-radius: 50%;  
179 - padding: 2px;  
180 -  
181 - &:hover {  
182 - color: #f00;  
183 - }  
184 - } 124 + margin-right: 10px;
185 } 125 }
186 - 126 +
  127 + .remove-icon {
  128 + position: absolute;
  129 + top: -10px;
  130 + right: -10px;
  131 + color: #f56c6c;
  132 + cursor: pointer;
  133 + font-size: 16px;
  134 + background: white;
  135 + border-radius: 50%;
  136 + padding: 2px;
  137 + }
  138 +
187 .upload-button { 139 .upload-button {
188 width: 100px; 140 width: 100px;
189 height: 100px; 141 height: 100px;
190 - border: 1px dashed #dcdfe6; 142 + border: 1px dashed #d9d9d9;
  143 + border-radius: 6px;
191 display: flex; 144 display: flex;
192 justify-content: center; 145 justify-content: center;
193 align-items: center; 146 align-items: center;
194 cursor: pointer; 147 cursor: pointer;
195 - color: #909399;  
196 - font-size: 24px;  
197 - 148 + color: #8c939d;
  149 + font-size: 28px;
  150 + background-color: #fbfdff;
  151 +
198 &:hover { 152 &:hover {
199 border-color: #409eff; 153 border-color: #409eff;
200 - color: #409eff;  
201 } 154 }
202 } 155 }
203 } 156 }
src/components/oa/viewImage.vue
1 <template> 1 <template>
2 - <el-dialog  
3 - :visible.sync="visible"  
4 - fullscreen  
5 - :show-close="false"  
6 - @close="handleClose"  
7 - >  
8 - <div style="position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);">  
9 - <el-image 2 + <div class="view-image-wrapper" v-show="visible">
  3 + <div class="image-container">
  4 + <img
10 :src="imageUrl" 5 :src="imageUrl"
11 - :style="imageStyle"  
12 - fit="contain"  
13 - />  
14 - <i  
15 - class="el-icon-close"  
16 - style="position: absolute; right: 20px; top: 20px; font-size: 24px; color: red; cursor: pointer;"  
17 - @click="visible = false" 6 + :style="{
  7 + width: imgWidth + 'px',
  8 + height: imgHeight + 'px'
  9 + }"
  10 + @error="handleImageError"
18 /> 11 />
  12 + <i class="el-icon-close close-icon" @click="close"></i>
19 </div> 13 </div>
20 - </el-dialog> 14 + </div>
21 </template> 15 </template>
22 16
23 <script> 17 <script>
@@ -27,35 +21,103 @@ export default { @@ -27,35 +21,103 @@ export default {
27 return { 21 return {
28 visible: false, 22 visible: false,
29 imageUrl: '', 23 imageUrl: '',
30 - imageStyle: {  
31 - width: '800px',  
32 - height: '800px'  
33 - } 24 + imgWidth: 800,
  25 + imgHeight: 800
34 } 26 }
35 }, 27 },
36 methods: { 28 methods: {
37 open(url) { 29 open(url) {
38 this.imageUrl = url 30 this.imageUrl = url
39 this.visible = true 31 this.visible = true
40 - this.adjustImageSize(url) 32 + this.calculateImageSize(url)
  33 + this.enterFullscreen()
  34 + },
  35 + close() {
  36 + this.visible = false
  37 + this.exitFullscreen()
41 }, 38 },
42 - handleClose() {  
43 - this.imageUrl = '' 39 + handleImageError() {
  40 + this.imageUrl = '/img/noPhoto.jpg'
44 }, 41 },
45 - adjustImageSize(url) { 42 + calculateImageSize(url) {
46 const img = new Image() 43 const img = new Image()
47 img.src = url 44 img.src = url
48 img.onload = () => { 45 img.onload = () => {
49 - const ratio = img.width / img.height  
50 - if (ratio > 1) {  
51 - this.imageStyle.height = `${800 / ratio}px`  
52 - this.imageStyle.width = '800px'  
53 - } else {  
54 - this.imageStyle.width = `${800 * ratio}px`  
55 - this.imageStyle.height = '800px'  
56 - } 46 + const imgScale = img.width / img.height
  47 + this.imgWidth = 800
  48 + this.imgHeight = 800 / imgScale
  49 + }
  50 + },
  51 + enterFullscreen() {
  52 + const element = document.querySelector('.view-image-wrapper')
  53 + if (element.requestFullscreen) {
  54 + element.requestFullscreen()
  55 + } else if (element.mozRequestFullScreen) {
  56 + element.mozRequestFullScreen()
  57 + } else if (element.webkitRequestFullscreen) {
  58 + element.webkitRequestFullscreen()
  59 + } else if (element.msRequestFullscreen) {
  60 + element.msRequestFullscreen()
  61 + }
  62 + },
  63 + exitFullscreen() {
  64 + if (document.exitFullscreen) {
  65 + document.exitFullscreen()
  66 + } else if (document.mozCancelFullScreen) {
  67 + document.mozCancelFullScreen()
  68 + } else if (document.webkitExitFullscreen) {
  69 + document.webkitExitFullscreen()
  70 + }
  71 + }
  72 + },
  73 + mounted() {
  74 + document.addEventListener('fullscreenchange', () => {
  75 + if (!document.fullscreenElement) {
  76 + this.close()
  77 + }
  78 + })
  79 + }
  80 +}
  81 +</script>
  82 +
  83 +<style lang="scss" scoped>
  84 +.view-image-wrapper {
  85 + position: fixed;
  86 + top: 0;
  87 + left: 0;
  88 + width: 100%;
  89 + height: 100%;
  90 + background-color: rgba(0, 0, 0, 0.8);
  91 + z-index: 9999;
  92 + display: flex;
  93 + justify-content: center;
  94 + align-items: center;
  95 +
  96 + .image-container {
  97 + position: relative;
  98 + background-color: #fff;
  99 + padding: 20px;
  100 + border-radius: 4px;
  101 +
  102 + img {
  103 + max-width: 90vw;
  104 + max-height: 90vh;
  105 + object-fit: contain;
  106 + }
  107 +
  108 + .close-icon {
  109 + position: absolute;
  110 + top: 10px;
  111 + right: 10px;
  112 + font-size: 24px;
  113 + color: #f56c6c;
  114 + cursor: pointer;
  115 + z-index: 1;
  116 +
  117 + &:hover {
  118 + color: #f78989;
57 } 119 }
58 } 120 }
59 } 121 }
60 } 122 }
61 -</script>  
62 \ No newline at end of file 123 \ No newline at end of file
  124 +</style>
63 \ No newline at end of file 125 \ No newline at end of file
src/i18n/oaI18n.js
@@ -27,6 +27,16 @@ import { messages as addExamineStaffMessages } from &#39;../views/oa/addExamineStaff @@ -27,6 +27,16 @@ import { messages as addExamineStaffMessages } from &#39;../views/oa/addExamineStaff
27 import { messages as editExamineStaffMessages } from '../views/oa/editExamineStaffLang' 27 import { messages as editExamineStaffMessages } from '../views/oa/editExamineStaffLang'
28 import { messages as examineStaffValueMessages } from '../views/oa/examineStaffValueLang' 28 import { messages as examineStaffValueMessages } from '../views/oa/examineStaffValueLang'
29 import { messages as oaWorkflowManageMessages } from '../views/oa/oaWorkflowManageLang' 29 import { messages as oaWorkflowManageMessages } from '../views/oa/oaWorkflowManageLang'
  30 +import { messages as newOaWorkflowManageMessages } from '../views/oa/newOaWorkflowManageLang'
  31 +import { messages as newOaWorkflowDoingMessages } from '../views/oa/newOaWorkflowDoingLang'
  32 +import { messages as attendanceClassesManageMessages } from '../views/oa/attendanceClassesManageLang'
  33 +import { messages as attendanceClassesStaffManageMessages } from '../views/oa/attendanceClassesStaffManageLang'
  34 +import { messages as todayAttendanceManageMessages } from '../views/oa/todayAttendanceManageLang'
  35 +import { messages as addAttendanceClassesStaffMessages } from '../views/oa/addAttendanceClassesStaffLang'
  36 +
  37 +import { messages as monthAttendanceManageMessages } from '../views/oa/monthAttendanceManageLang'
  38 +import { messages as staffAttendanceManageMessages } from '../views/oa/staffAttendanceManageLang'
  39 +import { messages as attendanceLogManageMessages } from '../views/oa/attendanceLogManageLang'
30 40
31 export const messages ={ 41 export const messages ={
32 en:{ 42 en:{
@@ -59,6 +69,15 @@ export const messages ={ @@ -59,6 +69,15 @@ export const messages ={
59 ...editExamineStaffMessages.en, 69 ...editExamineStaffMessages.en,
60 ...examineStaffValueMessages.en, 70 ...examineStaffValueMessages.en,
61 ...oaWorkflowManageMessages.en, 71 ...oaWorkflowManageMessages.en,
  72 + ...newOaWorkflowManageMessages.en,
  73 + ...newOaWorkflowDoingMessages.en,
  74 + ...attendanceClassesManageMessages.en,
  75 + ...attendanceClassesStaffManageMessages.en,
  76 + ...todayAttendanceManageMessages.en,
  77 + ...addAttendanceClassesStaffMessages.en,
  78 + ...monthAttendanceManageMessages.en,
  79 + ...staffAttendanceManageMessages.en,
  80 + ...attendanceLogManageMessages.en,
62 }, 81 },
63 zh:{ 82 zh:{
64 ...activitiesTypeManageMessages.zh, 83 ...activitiesTypeManageMessages.zh,
@@ -90,5 +109,14 @@ export const messages ={ @@ -90,5 +109,14 @@ export const messages ={
90 ...editExamineStaffMessages.zh, 109 ...editExamineStaffMessages.zh,
91 ...examineStaffValueMessages.zh, 110 ...examineStaffValueMessages.zh,
92 ...oaWorkflowManageMessages.zh, 111 ...oaWorkflowManageMessages.zh,
  112 + ...newOaWorkflowManageMessages.zh,
  113 + ...newOaWorkflowDoingMessages.zh,
  114 + ...attendanceClassesManageMessages.zh,
  115 + ...attendanceClassesStaffManageMessages.zh,
  116 + ...todayAttendanceManageMessages.zh,
  117 + ...addAttendanceClassesStaffMessages.zh,
  118 + ...monthAttendanceManageMessages.zh,
  119 + ...staffAttendanceManageMessages.zh,
  120 + ...attendanceLogManageMessages.zh,
93 } 121 }
94 } 122 }
95 \ No newline at end of file 123 \ No newline at end of file
src/router/oaRouter.js
@@ -125,8 +125,53 @@ export default [ @@ -125,8 +125,53 @@ export default [
125 component: () => import('@/views/oa/examineStaffValueList.vue') 125 component: () => import('@/views/oa/examineStaffValueList.vue')
126 }, 126 },
127 { 127 {
128 - path:'/pages/property/oaWorkflowManage',  
129 - name:'/pages/property/oaWorkflowManage', 128 + path: '/pages/property/oaWorkflowManage',
  129 + name: '/pages/property/oaWorkflowManage',
130 component: () => import('@/views/oa/oaWorkflowManageList.vue') 130 component: () => import('@/views/oa/oaWorkflowManageList.vue')
  131 + },
  132 + {
  133 + path: '/pages/property/newOaWorkflowManage',
  134 + name: '/pages/property/newOaWorkflowManage',
  135 + component: () => import('@/views/oa/newOaWorkflowManageList.vue')
  136 + },
  137 + {
  138 + path: '/pages/property/newOaWorkflowDoing',
  139 + name: '/pages/property/newOaWorkflowDoing',
  140 + component: () => import('@/views/oa/newOaWorkflowDoingList.vue')
  141 + },
  142 + {
  143 + path: '/pages/property/attendanceClassesManage',
  144 + name: '/pages/property/attendanceClassesManage',
  145 + component: () => import('@/views/oa/attendanceClassesManageList.vue')
  146 + },
  147 + {
  148 + path: '/pages/attendance/attendanceClassesStaffManage',
  149 + name: '/pages/attendance/attendanceClassesStaffManage',
  150 + component: () => import('@/views/oa/attendanceClassesStaffManageList.vue')
  151 + },
  152 + {
  153 + path: '/pages/property/todayAttendanceManage',
  154 + name: '/pages/property/todayAttendanceManage',
  155 + component: () => import('@/views/oa/todayAttendanceManageList.vue')
  156 + },
  157 + {
  158 + path: '/views/oa/addAttendanceClassesStaff',
  159 + name: '/views/oa/addAttendanceClassesStaff',
  160 + component: () => import('@/views/oa/addAttendanceClassesStaffList.vue')
  161 + },
  162 + {
  163 + path: '/pages/property/monthAttendanceManage',
  164 + name: '/pages/property/monthAttendanceManage',
  165 + component: () => import('@/views/oa/monthAttendanceManageList.vue')
  166 + },
  167 + {
  168 + path:'/pages/property/staffAttendanceManage',
  169 + name:'/pages/property/staffAttendanceManage',
  170 + component: () => import('@/views/oa/staffAttendanceManageList.vue')
131 }, 171 },
  172 + {
  173 + path:'/pages/property/attendanceLogManage',
  174 + name:'/pages/property/attendanceLogManage',
  175 + component: () => import('@/views/oa/attendanceLogManageList.vue')
  176 + },
132 ] 177 ]
133 \ No newline at end of file 178 \ No newline at end of file
src/views/oa/addAttendanceClassesStaffLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + addAttendanceClassesStaff: {
  4 + title: 'Add Attendance Classes Staff',
  5 + staffName: 'Staff Name',
  6 + staffNamePlaceholder: 'Required, please select staff',
  7 + staffNameRequired: 'Staff name is required',
  8 + attendanceFace: 'Attendance Face',
  9 + submitSuccess: 'Add success',
  10 + submitFailed: 'Add failed'
  11 + },
  12 + uploadImage: {
  13 + imageSizeLimit: 'Image size cannot exceed 2MB',
  14 + uploadFailed: 'Upload failed'
  15 + },
  16 + selectStaff: {
  17 + title: 'Select Staff',
  18 + orgInfo: 'Organization Information',
  19 + staffInfo: 'Staff Information',
  20 + submitter: 'Submitter',
  21 + dynamicAssign: 'Dynamic Assign',
  22 + loadStaffFailed: 'Failed to load staff list'
  23 + },
  24 + orgTree: {
  25 + loadFailed: 'Failed to load organization tree'
  26 + }
  27 + },
  28 + zh: {
  29 + addAttendanceClassesStaff: {
  30 + title: '添加考勤班次员工',
  31 + staffName: '员工名称',
  32 + staffNamePlaceholder: '必填,请选择员工',
  33 + staffNameRequired: '员工名称不能为空',
  34 + attendanceFace: '考勤人脸',
  35 + submitSuccess: '添加成功',
  36 + submitFailed: '添加失败'
  37 + },
  38 + uploadImage: {
  39 + imageSizeLimit: '图片大小不能超过2MB',
  40 + uploadFailed: '上传失败'
  41 + },
  42 + selectStaff: {
  43 + title: '选择员工',
  44 + orgInfo: '组织信息',
  45 + staffInfo: '员工信息',
  46 + submitter: '提交者',
  47 + dynamicAssign: '动态指定',
  48 + loadStaffFailed: '加载员工列表失败'
  49 + },
  50 + orgTree: {
  51 + loadFailed: '加载组织树失败'
  52 + }
  53 + }
  54 +}
0 \ No newline at end of file 55 \ No newline at end of file
src/views/oa/addAttendanceClassesStaffList.vue 0 → 100644
  1 +<template>
  2 + <div class="add-attendance-classes-staff-container">
  3 + <el-card>
  4 + <div slot="header" class="clearfix">
  5 + <span>{{ $t('addAttendanceClassesStaff.title') }}</span>
  6 + <el-button type="primary" size="small" class="float-right" @click="handleGoBack">
  7 + <i class="el-icon-close"></i>
  8 + {{ $t('common.back') }}
  9 + </el-button>
  10 + </div>
  11 +
  12 + <el-row>
  13 + <el-col :span="24">
  14 + <el-form label-width="120px">
  15 + <el-form-item :label="$t('addAttendanceClassesStaff.staffName')" class="text-left">
  16 + <el-input v-model="form.staffName" style="width: 80%;" :placeholder="$t('addAttendanceClassesStaff.staffNamePlaceholder')"
  17 + disabled></el-input>
  18 + <el-button type="primary" @click="handleChooseStaff" class="ml-10">
  19 + <i class="el-icon-search"></i>
  20 + {{ $t('common.select') }}
  21 + </el-button>
  22 + </el-form-item>
  23 +
  24 + <el-form-item :label="$t('addAttendanceClassesStaff.attendanceFace')">
  25 + <upload-image-url ref="uploadImage" :image-count="1"
  26 + @notifyUploadCoverImage="handleImageUpload"></upload-image-url>
  27 + </el-form-item>
  28 + </el-form>
  29 + </el-col>
  30 + </el-row>
  31 +
  32 + <div class="text-right mt-20">
  33 + <el-button type="warning" @click="handleGoBack" class="mr-20">
  34 + {{ $t('common.back') }}
  35 + </el-button>
  36 + <el-button type="primary" @click="handleSubmit">
  37 + <i class="el-icon-check"></i>
  38 + {{ $t('common.submit') }}
  39 + </el-button>
  40 + </div>
  41 + </el-card>
  42 +
  43 + <select-staff ref="selectStaff" @selectStaff="handleStaffChange"></select-staff>
  44 + </div>
  45 +</template>
  46 +
  47 +<script>
  48 +import UploadImageUrl from '@/components/upload/UploadImageUrl'
  49 +import SelectStaff from '@/components/staff/SelectStaff'
  50 +import { saveAttendanceClassesStaff } from '@/api/oa/addAttendanceClassesStaffApi'
  51 +import { getCommunityId } from '@/api/community/communityApi'
  52 +
  53 +export default {
  54 + name: 'AddAttendanceClassesStaffList',
  55 + components: {
  56 + UploadImageUrl,
  57 + SelectStaff
  58 + },
  59 + data() {
  60 + return {
  61 + form: {
  62 + csId: '',
  63 + classesId: '',
  64 + staffId: '',
  65 + staffName: '',
  66 + photo: '',
  67 + communityId: ''
  68 + }
  69 + }
  70 + },
  71 + created() {
  72 + this.form.classesId = this.$route.query.classesId
  73 + this.form.communityId = getCommunityId()
  74 + },
  75 + methods: {
  76 + handleChooseStaff() {
  77 + this.$refs.selectStaff.open(this.form)
  78 + },
  79 + handleStaffChange(staff) {
  80 + this.form.staffId = staff.staffId
  81 + this.form.staffName = staff.staffName
  82 + },
  83 + handleImageUpload(photos) {
  84 + if (photos.length > 0) {
  85 + this.form.photo = photos[0]
  86 + }
  87 + },
  88 + async handleSubmit() {
  89 + if (!this.validateForm()) {
  90 + return
  91 + }
  92 +
  93 + try {
  94 + const res = await saveAttendanceClassesStaff(this.form)
  95 + if (res.code === 0) {
  96 + this.$message.success(this.$t('common.addSuccess'))
  97 + this.handleGoBack()
  98 + } else {
  99 + this.$message.error(res.msg)
  100 + }
  101 + } catch (error) {
  102 + this.$message.error(this.$t('common.addFailed'))
  103 + }
  104 + },
  105 + validateForm() {
  106 + if (!this.form.staffName) {
  107 + this.$message.error(this.$t('addAttendanceClassesStaff.staffNameRequired'))
  108 + return false
  109 + }
  110 + return true
  111 + },
  112 + handleGoBack() {
  113 + this.$router.go(-1)
  114 + }
  115 + }
  116 +}
  117 +</script>
  118 +
  119 +<style lang="scss" scoped>
  120 +.add-attendance-classes-staff-container {
  121 + padding: 20px;
  122 +
  123 + .el-form-item {
  124 + margin-bottom: 22px;
  125 + }
  126 +
  127 + .ml-10 {
  128 + margin-left: 10px;
  129 + }
  130 +
  131 + .mt-20 {
  132 + margin-top: 20px;
  133 + }
  134 +
  135 + .mr-20 {
  136 + margin-right: 20px;
  137 + }
  138 +
  139 + .text-right {
  140 + text-align: right;
  141 + }
  142 +
  143 + .float-right {
  144 + float: right;
  145 + }
  146 +}
  147 +</style>
0 \ No newline at end of file 148 \ No newline at end of file
src/views/oa/attendanceClassesManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + attendanceClassesManage: {
  4 + title: 'Attendance Settings',
  5 + document: 'Document',
  6 + add: 'Add',
  7 + classesId: 'Classes ID',
  8 + classesName: 'Classes Name',
  9 + timeOffset: 'Clock-in Range',
  10 + lateOffset: 'Late Range',
  11 + leaveOffset: 'Early Leave Range',
  12 + maxLastOffset: 'Last Clock-out Range',
  13 + operation: 'Operation',
  14 + minute: 'minute',
  15 + fetchError: 'Failed to fetch attendance classes data'
  16 + },
  17 + addAttendanceClasses: {
  18 + title: 'Add Attendance Classes',
  19 + classesName: 'Classes Name',
  20 + classesNamePlaceholder: 'Required, please enter classes name',
  21 + classesNameRequired: 'Classes name is required',
  22 + classesNameMaxLength: 'Classes name format error',
  23 + timeOffset: 'Clock-in Range',
  24 + timeOffsetPlaceholder: 'Required, please enter clock-in range (minutes)',
  25 + timeOffsetRequired: 'Clock-in range is required',
  26 + timeOffsetFormat: 'Clock-in range format error',
  27 + lateOffset: 'Late Range',
  28 + lateOffsetPlaceholder: 'Required, please enter late range (minutes)',
  29 + lateOffsetRequired: 'Late range is required',
  30 + lateOffsetMaxLength: 'Late range error',
  31 + leaveOffset: 'Early Leave Range',
  32 + leaveOffsetPlaceholder: 'Required, please enter early leave range (minutes)',
  33 + leaveOffsetRequired: 'Early leave range is required',
  34 + leaveOffsetMaxLength: 'Early leave range error',
  35 + maxLastOffset: 'Last Clock-out Range',
  36 + maxLastOffsetPlaceholder: 'Required, please enter last clock-out range (minutes)',
  37 + maxLastOffsetRequired: 'Last clock-out range is required',
  38 + saveSuccess: 'Added successfully'
  39 + },
  40 + editAttendanceClasses: {
  41 + title: 'Edit Attendance Classes',
  42 + classesName: 'Classes Name',
  43 + classesNamePlaceholder: 'Required, please enter classes name',
  44 + classesNameRequired: 'Classes name is required',
  45 + classesNameMaxLength: 'Classes name format error',
  46 + timeOffset: 'Clock-in Range',
  47 + timeOffsetPlaceholder: 'Required, please enter clock-in range (minutes)',
  48 + timeOffsetRequired: 'Clock-in range is required',
  49 + timeOffsetFormat: 'Clock-in range format error',
  50 + lateOffset: 'Late Range',
  51 + lateOffsetPlaceholder: 'Required, please enter late range (minutes)',
  52 + lateOffsetRequired: 'Late range is required',
  53 + lateOffsetMaxLength: 'Late range error',
  54 + leaveOffset: 'Early Leave Range',
  55 + leaveOffsetPlaceholder: 'Required, please enter early leave range (minutes)',
  56 + leaveOffsetRequired: 'Early leave range is required',
  57 + leaveOffsetMaxLength: 'Early leave range error',
  58 + maxLastOffset: 'Last Clock-out Range',
  59 + maxLastOffsetPlaceholder: 'Required, please enter last clock-out range (minutes)',
  60 + maxLastOffsetRequired: 'Last clock-out range is required',
  61 + classesIdRequired: 'Classes ID is required',
  62 + editSuccess: 'Modified successfully'
  63 + },
  64 + deleteAttendanceClasses: {
  65 + title: 'Confirm Operation',
  66 + confirmText: 'Are you sure to delete this attendance classes?',
  67 + cancel: 'Cancel',
  68 + confirm: 'Confirm Delete',
  69 + deleteSuccess: 'Deleted successfully',
  70 + deleteError: 'Failed to delete attendance classes'
  71 + }
  72 + },
  73 + zh: {
  74 + attendanceClassesManage: {
  75 + title: '考勤设置',
  76 + document: '文档',
  77 + add: '添加',
  78 + classesId: '班组ID',
  79 + classesName: '班组名称',
  80 + timeOffset: '打卡范围',
  81 + lateOffset: '迟到范围',
  82 + leaveOffset: '早退范围',
  83 + maxLastOffset: '当日下班范围',
  84 + operation: '操作',
  85 + minute: '分钟',
  86 + fetchError: '获取考勤班组数据失败'
  87 + },
  88 + addAttendanceClasses: {
  89 + title: '添加考勤班组',
  90 + classesName: '班次名称',
  91 + classesNamePlaceholder: '必填,请填写班次名称',
  92 + classesNameRequired: '班次名称不能为空',
  93 + classesNameMaxLength: '班次名称格式错误',
  94 + timeOffset: '打卡范围',
  95 + timeOffsetPlaceholder: '必填,请填写打卡范围(分钟)',
  96 + timeOffsetRequired: '打卡范围不能为空',
  97 + timeOffsetFormat: '打卡范围格式错误',
  98 + lateOffset: '迟到范围',
  99 + lateOffsetPlaceholder: '必填,请填写迟到范围(分钟)',
  100 + lateOffsetRequired: '迟到范围不能为空',
  101 + lateOffsetMaxLength: '迟到范围错误',
  102 + leaveOffset: '早退范围',
  103 + leaveOffsetPlaceholder: '必填,请填写早退范围(分钟)',
  104 + leaveOffsetRequired: '早退范围不能为空',
  105 + leaveOffsetMaxLength: '早退范围错误',
  106 + maxLastOffset: '当日下班范围',
  107 + maxLastOffsetPlaceholder: '必填,请填写当日最后一次下班打卡范围(分钟)',
  108 + maxLastOffsetRequired: '当日下班范围不能为空',
  109 + saveSuccess: '添加成功'
  110 + },
  111 + editAttendanceClasses: {
  112 + title: '修改考勤班组',
  113 + classesName: '班次名称',
  114 + classesNamePlaceholder: '必填,请填写班次名称',
  115 + classesNameRequired: '班次名称不能为空',
  116 + classesNameMaxLength: '班次名称格式错误',
  117 + timeOffset: '打卡范围',
  118 + timeOffsetPlaceholder: '必填,请填写打卡范围(分钟)',
  119 + timeOffsetRequired: '打卡范围不能为空',
  120 + timeOffsetFormat: '打卡范围格式错误',
  121 + lateOffset: '迟到范围',
  122 + lateOffsetPlaceholder: '必填,请填写迟到范围(分钟)',
  123 + lateOffsetRequired: '迟到范围不能为空',
  124 + lateOffsetMaxLength: '迟到范围错误',
  125 + leaveOffset: '早退范围',
  126 + leaveOffsetPlaceholder: '必填,请填写早退范围(分钟)',
  127 + leaveOffsetRequired: '早退范围不能为空',
  128 + leaveOffsetMaxLength: '早退范围错误',
  129 + maxLastOffset: '当日下班范围',
  130 + maxLastOffsetPlaceholder: '必填,请填写当日最后一次下班打卡范围(分钟)',
  131 + maxLastOffsetRequired: '当日下班范围不能为空',
  132 + classesIdRequired: '班组ID不能为空',
  133 + editSuccess: '修改成功'
  134 + },
  135 + deleteAttendanceClasses: {
  136 + title: '请确认您的操作',
  137 + confirmText: '确定删除考勤班组吗?',
  138 + cancel: '点错了',
  139 + confirm: '确认删除',
  140 + deleteSuccess: '删除成功',
  141 + deleteError: '删除考勤班组失败'
  142 + }
  143 + }
  144 +}
0 \ No newline at end of file 145 \ No newline at end of file
src/views/oa/attendanceClassesManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="attendance-classes-manage-container">
  3 + <el-row>
  4 + <el-col :span="24">
  5 + <el-card>
  6 + <div slot="header" class="flex justify-between">
  7 + <span>{{ $t('attendanceClassesManage.title') }}</span>
  8 + <div class="header-tools">
  9 + <el-button type="primary" size="small" @click="showMarkdown">
  10 + <i class="el-icon-document"></i>
  11 + {{ $t('attendanceClassesManage.document') }}
  12 + </el-button>
  13 + <el-button type="primary" size="small" @click="openAddAttendanceClassesModal">
  14 + <i class="el-icon-plus"></i>
  15 + {{ $t('attendanceClassesManage.add') }}
  16 + </el-button>
  17 + </div>
  18 + </div>
  19 +
  20 + <el-table :data="attendanceClassesManageInfo.attendanceClassess" border style="width: 100%"
  21 + v-loading="loading">
  22 + <el-table-column prop="classesId" :label="$t('attendanceClassesManage.classesId')" align="center" />
  23 + <el-table-column prop="classesName" :label="$t('attendanceClassesManage.classesName')" align="center" />
  24 + <el-table-column :label="$t('attendanceClassesManage.timeOffset')" align="center">
  25 + <template slot-scope="scope">
  26 + {{ scope.row.timeOffset }} {{ $t('attendanceClassesManage.minute') }}
  27 + </template>
  28 + </el-table-column>
  29 + <el-table-column :label="$t('attendanceClassesManage.lateOffset')" align="center">
  30 + <template slot-scope="scope">
  31 + {{ scope.row.lateOffset }} {{ $t('attendanceClassesManage.minute') }}
  32 + </template>
  33 + </el-table-column>
  34 + <el-table-column :label="$t('attendanceClassesManage.leaveOffset')" align="center">
  35 + <template slot-scope="scope">
  36 + {{ scope.row.leaveOffset }} {{ $t('attendanceClassesManage.minute') }}
  37 + </template>
  38 + </el-table-column>
  39 + <el-table-column :label="$t('attendanceClassesManage.maxLastOffset')" align="center">
  40 + <template slot-scope="scope">
  41 + {{ scope.row.maxLastOffset }} {{ $t('attendanceClassesManage.minute') }}
  42 + </template>
  43 + </el-table-column>
  44 + <el-table-column :label="$t('common.operation')" align="center" width="200">
  45 + <template slot-scope="scope">
  46 + <el-button size="mini" @click="openEditAttendanceClassesModel(scope.row)">
  47 + {{ $t('common.edit') }}
  48 + </el-button>
  49 + <el-button size="mini" type="danger" @click="openDeleteAttendanceClassesModel(scope.row)">
  50 + {{ $t('common.delete') }}
  51 + </el-button>
  52 + </template>
  53 + </el-table-column>
  54 + </el-table>
  55 +
  56 + <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
  57 + :current-page="page.current" :page-sizes="[10, 20, 30, 50]" :page-size="page.size"
  58 + layout="total, sizes, prev, pager, next, jumper" :total="page.total" class="pagination" />
  59 + </el-card>
  60 + </el-col>
  61 + </el-row>
  62 +
  63 + <add-attendance-classes ref="addAttendanceClasses" @success="listAttendanceClasses" />
  64 + <edit-attendance-classes ref="editAttendanceClasses" @success="listAttendanceClasses" />
  65 + <delete-attendance-classes ref="deleteAttendanceClasses" @success="listAttendanceClasses" />
  66 + </div>
  67 +</template>
  68 +
  69 +<script>
  70 +import { listAttendanceClassess } from '@/api/oa/attendanceClassesManageApi'
  71 +import AddAttendanceClasses from '@/components/oa/addAttendanceClasses'
  72 +import EditAttendanceClasses from '@/components/oa/editAttendanceClasses'
  73 +import DeleteAttendanceClasses from '@/components/oa/deleteAttendanceClasses'
  74 +import { getCommunityId } from '@/api/community/communityApi'
  75 +
  76 +export default {
  77 + name: 'AttendanceClassesManageList',
  78 + components: {
  79 + AddAttendanceClasses,
  80 + EditAttendanceClasses,
  81 + DeleteAttendanceClasses
  82 + },
  83 + data() {
  84 + return {
  85 + loading: false,
  86 + attendanceClassesManageInfo: {
  87 + attendanceClassess: [],
  88 + classesObjId: '',
  89 + classesObjName: ''
  90 + },
  91 + page: {
  92 + current: 1,
  93 + size: 10,
  94 + total: 0
  95 + }
  96 + }
  97 + },
  98 + created() {
  99 + this.communityId = getCommunityId()
  100 + this.listAttendanceClasses()
  101 + },
  102 + methods: {
  103 + async listAttendanceClasses() {
  104 + try {
  105 + this.loading = true
  106 + const params = {
  107 + page: this.page.current,
  108 + row: this.page.size,
  109 + communityId: this.communityId
  110 + }
  111 + const { data, total } = await listAttendanceClassess(params)
  112 + this.attendanceClassesManageInfo.attendanceClassess = data
  113 + this.page.total = total
  114 + } catch (error) {
  115 + this.$message.error(this.$t('attendanceClassesManage.fetchError'))
  116 + } finally {
  117 + this.loading = false
  118 + }
  119 + },
  120 + showMarkdown() {
  121 + // TODO: 实现文档显示逻辑
  122 + },
  123 + openAddAttendanceClassesModal() {
  124 + this.$refs.addAttendanceClasses.open({
  125 + classesObjId: this.attendanceClassesManageInfo.classesObjId,
  126 + classesObjName: this.attendanceClassesManageInfo.classesObjName
  127 + })
  128 + },
  129 + openEditAttendanceClassesModel(row) {
  130 + this.$refs.editAttendanceClasses.open(row)
  131 + },
  132 + openDeleteAttendanceClassesModel(row) {
  133 + this.$refs.deleteAttendanceClasses.open(row)
  134 + },
  135 + handleSizeChange(val) {
  136 + this.page.size = val
  137 + this.listAttendanceClasses()
  138 + },
  139 + handleCurrentChange(val) {
  140 + this.page.current = val
  141 + this.listAttendanceClasses()
  142 + }
  143 + }
  144 +}
  145 +</script>
  146 +
  147 +<style lang="scss" scoped>
  148 +.attendance-classes-manage-container {
  149 + padding: 20px;
  150 +
  151 + .header-tools {
  152 + float: right;
  153 + margin-top: -5px;
  154 + }
  155 +
  156 + .pagination {
  157 + margin-top: 20px;
  158 + text-align: right;
  159 + }
  160 +}
  161 +</style>
0 \ No newline at end of file 162 \ No newline at end of file
src/views/oa/attendanceClassesStaffManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + attendanceClassesStaffManage: {
  4 + search: {
  5 + title: 'Search Conditions',
  6 + staffName: 'Please enter staff name'
  7 + },
  8 + list: {
  9 + title: 'Attendance Staff List'
  10 + },
  11 + table: {
  12 + face: 'Face',
  13 + staffId: 'Staff ID',
  14 + staffName: 'Staff Name',
  15 + createTime: 'Create Time'
  16 + },
  17 + delete: {
  18 + title: 'Confirm Operation',
  19 + confirmText: 'Are you sure to delete this attendance staff?',
  20 + success: 'Delete successfully',
  21 + error: 'Delete failed'
  22 + },
  23 + fetchError: 'Failed to fetch attendance staff data',
  24 + fetchClassError: 'Failed to fetch attendance classes data'
  25 + }
  26 + },
  27 + zh: {
  28 + attendanceClassesStaffManage: {
  29 + search: {
  30 + title: '查询条件',
  31 + staffName: '请输入员工姓名'
  32 + },
  33 + list: {
  34 + title: '考勤员工列表'
  35 + },
  36 + table: {
  37 + face: '人脸',
  38 + staffId: '员工编号',
  39 + staffName: '员工名称',
  40 + createTime: '创建时间'
  41 + },
  42 + delete: {
  43 + title: '确认操作',
  44 + confirmText: '确定删除该考勤员工吗?',
  45 + success: '删除成功',
  46 + error: '删除失败'
  47 + },
  48 + fetchError: '获取考勤员工数据失败',
  49 + fetchClassError: '获取考勤班次数据失败'
  50 + }
  51 + }
  52 +}
0 \ No newline at end of file 53 \ No newline at end of file
src/views/oa/attendanceClassesStaffManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="attendance-classes-staff-manage-container">
  3 + <el-row :gutter="20">
  4 + <el-col :span="4">
  5 + <el-card class="tree-card">
  6 + <div class="treeview attendance-staff">
  7 + <ul class="list-group">
  8 + <li v-for="(item, index) in attendanceClassesStaffManageInfo.attendanceClassess" :key="index"
  9 + class="list-group-item node-orgTree"
  10 + :class="{ 'vc-node-selected': attendanceClassesStaffManageInfo.conditions.classesId == item.classesId }"
  11 + @click="swatchClass(item)">
  12 + {{ item.classesName }}
  13 + </li>
  14 + </ul>
  15 + </div>
  16 + </el-card>
  17 + </el-col>
  18 + <el-col :span="20">
  19 + <el-card>
  20 + <div slot="header" class="flex justify-between">
  21 + <span>{{ $t('attendanceClassesStaffManage.search.title') }}</span>
  22 + </div>
  23 + <el-row :gutter="20">
  24 + <el-col :span="8">
  25 + <el-input v-model="attendanceClassesStaffManageInfo.conditions.staffNameLike"
  26 + :placeholder="$t('attendanceClassesStaffManage.search.staffName')" clearable />
  27 + </el-col>
  28 + <el-col :span="16">
  29 + <el-button type="primary" @click="_queryAttendanceClassesStaffMethod">
  30 + <i class="el-icon-search"></i>
  31 + {{ $t('common.search') }}
  32 + </el-button>
  33 + <el-button @click="_resetAttendanceClassesStaffMethod">
  34 + <i class="el-icon-refresh"></i>
  35 + {{ $t('common.reset') }}
  36 + </el-button>
  37 + </el-col>
  38 + </el-row>
  39 + </el-card>
  40 +
  41 + <el-card style="margin-top: 20px;">
  42 + <div slot="header" class="flex justify-between">
  43 + <span>{{ $t('attendanceClassesStaffManage.list.title') }}</span>
  44 + <el-button type="primary" size="small" style="float: right;" @click="_openAddAttendanceClassesStaffModal">
  45 + <i class="el-icon-plus"></i>
  46 + {{ $t('common.add') }}
  47 + </el-button>
  48 + </div>
  49 + <el-table :data="attendanceClassesStaffManageInfo.attendanceClassesStaffs" border style="width: 100%"
  50 + v-loading="loading">
  51 + <el-table-column prop="personFace" :label="$t('attendanceClassesStaffManage.table.face')" align="center"
  52 + width="100">
  53 + <template slot-scope="scope">
  54 + <div style="position: relative; display: inline-block;"
  55 + @click="showImg(scope.row.personFace || '/img/noPhoto.jpg')">
  56 + <el-image style="width: 50px; height: 50px" :src="scope.row.personFace || '/img/noPhoto.jpg'"
  57 + fit="cover">
  58 + <div slot="error" class="image-slot">
  59 + <img src="/img/noPhoto.jpg" style="width: 50px; height: 50px">
  60 + </div>
  61 + </el-image>
  62 + <img src="/img/icon-bigimg.png" style="position: absolute;right: 0;bottom: 0;" width="50" height="50"
  63 + alt="">
  64 + </div>
  65 + </template>
  66 + </el-table-column>
  67 + <el-table-column prop="staffId" :label="$t('attendanceClassesStaffManage.table.staffId')" align="center" />
  68 + <el-table-column prop="staffName" :label="$t('attendanceClassesStaffManage.table.staffName')"
  69 + align="center" />
  70 + <el-table-column prop="createTime" :label="$t('attendanceClassesStaffManage.table.createTime')"
  71 + align="center" />
  72 + <el-table-column :label="$t('common.operation')" align="center" width="200">
  73 + <template slot-scope="scope">
  74 + <el-button size="mini" type="danger" @click="_openDeleteAttendanceClassesStaffModel(scope.row)">
  75 + {{ $t('common.delete') }}
  76 + </el-button>
  77 + <el-button size="mini" type="primary" @click="openStaffDetail(scope.row)">
  78 + {{ $t('common.detail') }}
  79 + </el-button>
  80 + </template>
  81 + </el-table-column>
  82 + </el-table>
  83 +
  84 + <el-pagination :current-page.sync="page.current" :page-sizes="[10, 20, 30, 50]" :page-size="page.size"
  85 + :total="page.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  86 + @current-change="handleCurrentChange" style="margin-top: 20px;" />
  87 + </el-card>
  88 + </el-col>
  89 + </el-row>
  90 +
  91 + <delete-attendance-classes-staff ref="deleteDialog" @success="handleSuccess" />
  92 + <view-image ref="viewImage" />
  93 + </div>
  94 +</template>
  95 +
  96 +<script>
  97 +import { listAttendanceClassesStaff, listAttendanceClassess } from '@/api/oa/attendanceClassesStaffManageApi'
  98 +import DeleteAttendanceClassesStaff from '@/components/oa/deleteAttendanceClassesStaff'
  99 +import ViewImage from '@/components/system/viewImage'
  100 +import { getCommunityId } from '@/api/community/communityApi'
  101 +
  102 +export default {
  103 + name: 'AttendanceClassesStaffManageList',
  104 + components: {
  105 + DeleteAttendanceClassesStaff,
  106 + ViewImage
  107 + },
  108 + data() {
  109 + return {
  110 + loading: false,
  111 + attendanceClassesStaffManageInfo: {
  112 + attendanceClassesStaffs: [],
  113 + total: 0,
  114 + records: 1,
  115 + moreCondition: false,
  116 + csId: '',
  117 + attendanceClassess: [],
  118 + conditions: {
  119 + classesId: '',
  120 + staffId: '',
  121 + staffNameLike: '',
  122 + page: 1,
  123 + row: 10
  124 + }
  125 + },
  126 + page: {
  127 + current: 1,
  128 + size: 10,
  129 + total: 0
  130 + }
  131 + }
  132 + },
  133 + created() {
  134 + this.communityId = getCommunityId()
  135 + this._loadAttendanceClass()
  136 + },
  137 + methods: {
  138 + async _listAttendanceClassesStaffs(page = 1, rows = 10) {
  139 + try {
  140 + this.loading = true
  141 + this.attendanceClassesStaffManageInfo.conditions.page = page
  142 + this.attendanceClassesStaffManageInfo.conditions.row = rows
  143 +
  144 + const { data, total } = await listAttendanceClassesStaff(this.attendanceClassesStaffManageInfo.conditions)
  145 + this.attendanceClassesStaffManageInfo.attendanceClassesStaffs = data
  146 + this.page.total = total
  147 + } catch (error) {
  148 + console.error('获取考勤员工列表失败:', error)
  149 + this.$message.error(this.$t('attendanceClassesStaffManage.fetchError'))
  150 + } finally {
  151 + this.loading = false
  152 + }
  153 + },
  154 + _openAddAttendanceClassesStaffModal() {
  155 + this.$router.push(`/views/oa/addAttendanceClassesStaff?classesId=${this.attendanceClassesStaffManageInfo.conditions.classesId}`)
  156 + },
  157 + _openDeleteAttendanceClassesStaffModel(row) {
  158 + this.$refs.deleteDialog.open(row)
  159 + },
  160 + _queryAttendanceClassesStaffMethod() {
  161 + this.page.current = 1
  162 + this._listAttendanceClassesStaffs()
  163 + },
  164 + _resetAttendanceClassesStaffMethod() {
  165 + this.attendanceClassesStaffManageInfo.conditions.staffNameLike = ''
  166 + this._queryAttendanceClassesStaffMethod()
  167 + },
  168 + swatchClass(item) {
  169 + this.attendanceClassesStaffManageInfo.conditions.classesId = item.classesId
  170 + this._listAttendanceClassesStaffs()
  171 + },
  172 + async _loadAttendanceClass() {
  173 + try {
  174 + const { data } = await listAttendanceClassess({ page: 1, row: 100 })
  175 + this.attendanceClassesStaffManageInfo.attendanceClassess = data
  176 + if (data && data.length > 0) {
  177 + this.swatchClass(data[0])
  178 + }
  179 + } catch (error) {
  180 + console.error('获取考勤班次失败:', error)
  181 + this.$message.error(this.$t('attendanceClassesStaffManage.fetchClassError'))
  182 + }
  183 + },
  184 + showImg(url) {
  185 + this.$refs.viewImage.open(url)
  186 + },
  187 + openStaffDetail(staff) {
  188 + this.$router.push(`/staff/staffDetail?staffId=${staff.staffId}`)
  189 + },
  190 + handleSizeChange(val) {
  191 + this.page.size = val
  192 + this._listAttendanceClassesStaffs(this.page.current, val)
  193 + },
  194 + handleCurrentChange(val) {
  195 + this._listAttendanceClassesStaffs(val, this.page.size)
  196 + },
  197 + handleSuccess() {
  198 + this._listAttendanceClassesStaffs(this.page.current, this.page.size)
  199 + }
  200 + }
  201 +}
  202 +</script>
  203 +
  204 +<style lang="scss" scoped>
  205 +.attendance-classes-staff-manage-container {
  206 + padding: 20px;
  207 +
  208 + .tree-card {
  209 + height: 100%;
  210 +
  211 + .treeview {
  212 + height: 100%;
  213 +
  214 + .list-group {
  215 + list-style: none;
  216 + padding: 0;
  217 + margin: 0;
  218 +
  219 + .list-group-item {
  220 + padding: 10px 15px;
  221 + margin-bottom: 5px;
  222 + border: 1px solid #ddd;
  223 + border-radius: 4px;
  224 + cursor: pointer;
  225 + text-align: center;
  226 +
  227 + &:hover {
  228 + background-color: #f5f5f5;
  229 + }
  230 +
  231 + &.vc-node-selected {
  232 + background-color: #409EFF;
  233 + color: white;
  234 + border-color: #409EFF;
  235 + }
  236 + }
  237 + }
  238 + }
  239 + }
  240 +}
  241 +</style>
0 \ No newline at end of file 242 \ No newline at end of file
src/views/oa/attendanceLogManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + attendanceLogManage: {
  4 + search: {
  5 + title: 'Search Conditions',
  6 + classesName: 'Please enter class name',
  7 + departmentName: 'Please enter department name',
  8 + date: 'Please select clock time',
  9 + staffName: 'Please enter staff name'
  10 + },
  11 + list: {
  12 + title: 'Attendance Records'
  13 + },
  14 + table: {
  15 + face: 'Face',
  16 + departmentName: 'Department Name',
  17 + staffName: 'Staff Name',
  18 + staffId: 'Staff ID',
  19 + clockTime: 'Clock Time',
  20 + remark: 'Remark'
  21 + },
  22 + fetchError: 'Failed to fetch attendance records'
  23 + }
  24 + },
  25 + zh: {
  26 + attendanceLogManage: {
  27 + search: {
  28 + title: '查询条件',
  29 + classesName: '请输入班组名称',
  30 + departmentName: '请输入部门名称',
  31 + date: '请选择打卡时间',
  32 + staffName: '请输入员工名称'
  33 + },
  34 + list: {
  35 + title: '考勤记录'
  36 + },
  37 + table: {
  38 + face: '人脸',
  39 + departmentName: '部门名称',
  40 + staffName: '员工名称',
  41 + staffId: '员工ID',
  42 + clockTime: '考勤时间',
  43 + remark: '说明'
  44 + },
  45 + fetchError: '获取考勤记录失败'
  46 + }
  47 + }
  48 +}
0 \ No newline at end of file 49 \ No newline at end of file
src/views/oa/attendanceLogManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="attendance-log-manage-container">
  3 + <!-- 查询条件 -->
  4 + <el-card class="search-wrapper">
  5 + <div slot="header" class="flex justify-between">
  6 + <span>{{ $t('attendanceLogManage.search.title') }}</span>
  7 + </div>
  8 + <el-row :gutter="20">
  9 + <el-col :span="4">
  10 + <el-input v-model="searchForm.classesName" :placeholder="$t('attendanceLogManage.search.classesName')"
  11 + clearable />
  12 + </el-col>
  13 + <el-col :span="4">
  14 + <el-input v-model="searchForm.departmentName" :placeholder="$t('attendanceLogManage.search.departmentName')"
  15 + clearable />
  16 + </el-col>
  17 + <el-col :span="4">
  18 + <el-date-picker v-model="searchForm.date" type="date" :placeholder="$t('attendanceLogManage.search.date')"
  19 + value-format="yyyy-MM-dd" style="width: 100%" />
  20 + </el-col>
  21 + <el-col :span="4">
  22 + <el-input v-model="searchForm.staffName" :placeholder="$t('attendanceLogManage.search.staffName')"
  23 + clearable />
  24 + </el-col>
  25 + <el-col :span="4">
  26 + <el-button type="primary" @click="handleSearch">{{ $t('common.search') }}</el-button>
  27 + <el-button @click="handleReset">{{ $t('common.reset') }}</el-button>
  28 + </el-col>
  29 + </el-row>
  30 + </el-card>
  31 +
  32 + <!-- 考勤记录列表 -->
  33 + <el-card class="list-wrapper">
  34 + <div slot="header" class="flex justify-between">
  35 + <span>{{ $t('attendanceLogManage.list.title') }}</span>
  36 + </div>
  37 + <el-table v-loading="loading" :data="tableData" border style="width: 100%">
  38 + <el-table-column :label="$t('attendanceLogManage.table.face')" align="center" width="100">
  39 + <template slot-scope="scope">
  40 + <el-image style="width: 60px; height: 60px; cursor: pointer;"
  41 + :src="scope.row.facePath || '/img/noPhoto.jpg'" fit="cover"
  42 + @click="handleViewImage(scope.row.facePath)" />
  43 + </template>
  44 + </el-table-column>
  45 + <el-table-column prop="departmentName" :label="$t('attendanceLogManage.table.departmentName')" align="center" />
  46 + <el-table-column prop="staffName" :label="$t('attendanceLogManage.table.staffName')" align="center" />
  47 + <el-table-column prop="staffId" :label="$t('attendanceLogManage.table.staffId')" align="center" />
  48 + <el-table-column prop="clockTime" :label="$t('attendanceLogManage.table.clockTime')" align="center" />
  49 + <el-table-column prop="remark" :label="$t('attendanceLogManage.table.remark')" align="center" />
  50 + </el-table>
  51 +
  52 + <el-pagination :current-page.sync="page.current" :page-sizes="[10, 20, 30, 50]" :page-size="page.size"
  53 + :total="page.total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
  54 + @current-change="handleCurrentChange" />
  55 + </el-card>
  56 +
  57 + <!-- 图片查看组件 -->
  58 + <view-image ref="viewImage" />
  59 + </div>
  60 +</template>
  61 +
  62 +<script>
  63 +import { queryAttendanceLog } from '@/api/oa/attendanceLogManageApi'
  64 +import ViewImage from '@/components/system/viewImage'
  65 +
  66 +export default {
  67 + name: 'AttendanceLogManageList',
  68 + components: {
  69 + ViewImage
  70 + },
  71 + data() {
  72 + return {
  73 + loading: false,
  74 + searchForm: {
  75 + classesName: '',
  76 + departmentName: '',
  77 + date: new Date().toISOString().split('T')[0],
  78 + staffName: ''
  79 + },
  80 + tableData: [],
  81 + page: {
  82 + current: 1,
  83 + size: 10,
  84 + total: 0
  85 + }
  86 + }
  87 + },
  88 + created() {
  89 + this.getList()
  90 + },
  91 + methods: {
  92 + async getList() {
  93 + try {
  94 + this.loading = true
  95 + const params = {
  96 + page: this.page.current,
  97 + row: this.page.size,
  98 + ...this.searchForm
  99 + }
  100 + const { data, total } = await queryAttendanceLog(params)
  101 + this.tableData = data
  102 + this.page.total = total
  103 + } catch (error) {
  104 + this.$message.error(this.$t('attendanceLogManage.fetchError'))
  105 + } finally {
  106 + this.loading = false
  107 + }
  108 + },
  109 + handleSearch() {
  110 + this.page.current = 1
  111 + this.getList()
  112 + },
  113 + handleReset() {
  114 + this.searchForm = {
  115 + classesName: '',
  116 + departmentName: '',
  117 + date: new Date().toISOString().split('T')[0],
  118 + staffName: ''
  119 + }
  120 + this.handleSearch()
  121 + },
  122 + handleSizeChange(val) {
  123 + this.page.size = val
  124 + this.getList()
  125 + },
  126 + handleCurrentChange(val) {
  127 + this.page.current = val
  128 + this.getList()
  129 + },
  130 + handleViewImage(url) {
  131 + if (url) {
  132 + this.$refs.viewImage.open(url)
  133 + }
  134 + }
  135 + }
  136 +}
  137 +</script>
  138 +
  139 +<style lang="scss" scoped>
  140 +.attendance-log-manage-container {
  141 + padding: 20px;
  142 +
  143 + .search-wrapper {
  144 + margin-bottom: 20px;
  145 +
  146 + .el-row {
  147 + margin-bottom: -20px;
  148 + }
  149 +
  150 + .el-col {
  151 + margin-bottom: 20px;
  152 + }
  153 + }
  154 +
  155 + .list-wrapper {
  156 + .el-pagination {
  157 + margin-top: 20px;
  158 + text-align: right;
  159 + }
  160 + }
  161 +}
  162 +</style>
0 \ No newline at end of file 163 \ No newline at end of file
src/views/oa/monthAttendanceManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + monthAttendance: {
  4 + search: {
  5 + title: 'Search Conditions',
  6 + classes: 'Classes',
  7 + selectClasses: 'Please select classes',
  8 + staffName: 'Staff Name',
  9 + inputStaffName: 'Please input staff name',
  10 + date: 'Date',
  11 + selectDate: 'Please select date'
  12 + },
  13 + table: {
  14 + title: 'Monthly Attendance',
  15 + classes: 'Classes',
  16 + staffName: 'Staff Name',
  17 + clockIn: 'Normal Attendance',
  18 + late: 'Late',
  19 + early: 'Leave Early',
  20 + noClockIn: 'Absenteeism',
  21 + free: 'No Need Attendance'
  22 + },
  23 + work: 'Work',
  24 + offWork: 'Off Work',
  25 + notTime: 'Not time yet',
  26 + noNeed: 'No need attendance',
  27 + fetchError: 'Failed to fetch attendance data',
  28 + fetchClassesError: 'Failed to fetch classes data',
  29 + exportSuccess: 'Export successfully',
  30 + exportError: 'Failed to export'
  31 + }
  32 + },
  33 + zh: {
  34 + monthAttendance: {
  35 + search: {
  36 + title: '查询条件',
  37 + classes: '班次',
  38 + selectClasses: '请选择班次',
  39 + staffName: '员工名称',
  40 + inputStaffName: '请输入员工名称',
  41 + date: '打卡时间',
  42 + selectDate: '请选择打卡时间'
  43 + },
  44 + table: {
  45 + title: '月考勤',
  46 + classes: '考勤组',
  47 + staffName: '员工名称',
  48 + clockIn: '正常考勤',
  49 + late: '迟到',
  50 + early: '早退',
  51 + noClockIn: '旷工',
  52 + free: '免考勤'
  53 + },
  54 + work: '上班',
  55 + offWork: '下班',
  56 + notTime: '未到时间',
  57 + noNeed: '无需考勤',
  58 + fetchError: '获取考勤数据失败',
  59 + fetchClassesError: '获取班次数据失败',
  60 + exportSuccess: '导出成功',
  61 + exportError: '导出失败'
  62 + }
  63 + }
  64 +}
0 \ No newline at end of file 65 \ No newline at end of file
src/views/oa/monthAttendanceManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="month-attendance-container">
  3 + <!-- 查询条件 -->
  4 + <el-card class="search-wrapper">
  5 + <div slot="header" class="flex justify-between">
  6 + <span>{{ $t('monthAttendance.search.title') }}</span>
  7 + </div>
  8 + <el-row :gutter="20">
  9 + <el-col :span="6">
  10 + <el-select v-model="searchForm.classesId" :placeholder="$t('monthAttendance.search.selectClasses')"
  11 + style="width:100%">
  12 + <el-option v-for="item in attendanceClasses" :key="item.classesId" :label="item.classesName"
  13 + :value="item.classesId" />
  14 + </el-select>
  15 + </el-col>
  16 + <el-col :span="6">
  17 + <el-input v-model="searchForm.staffName" :placeholder="$t('monthAttendance.search.inputStaffName')" />
  18 + </el-col>
  19 + <el-col :span="6">
  20 + <el-date-picker v-model="searchForm.date" type="month"
  21 + :placeholder="$t('monthAttendance.search.selectDate')" value-format="yyyy-MM" style="width:100%"
  22 + @change="handleDateChange" />
  23 + </el-col>
  24 + <el-col :span="6" class="text-right">
  25 + <el-button type="primary" @click="handleSearch">{{ $t('common.search') }}</el-button>
  26 + <el-button @click="handleReset">{{ $t('common.reset') }}</el-button>
  27 + </el-col>
  28 + </el-row>
  29 + </el-card>
  30 +
  31 + <!-- 数据表格 -->
  32 + <el-card class="table-wrapper">
  33 + <div slot="header" class="flex justify-between">
  34 + <span>{{ $t('monthAttendance.table.title') }}</span>
  35 + <el-button type="primary" size="small" class="float-right" @click="handleExport">
  36 + {{ $t('common.export') }}
  37 + </el-button>
  38 + </div>
  39 +
  40 + <div class="table-content" :style="{ width: tableWidth }">
  41 + <el-table :data="tableData" border style="width: 100%" v-loading="loading">
  42 + <el-table-column prop="classesName" :label="$t('monthAttendance.table.classes')" align="center" />
  43 + <el-table-column prop="staffName" :label="$t('monthAttendance.table.staffName')" align="center" />
  44 + <el-table-column v-for="day in curDays" :key="day" :label="`${day}日`" align="center">
  45 + <template slot-scope="scope">
  46 + <span v-html="getDayDetail(scope.row.days, day)"></span>
  47 + </template>
  48 + </el-table-column>
  49 + <el-table-column prop="clockIn" :label="$t('monthAttendance.table.clockIn')" align="center" />
  50 + <el-table-column prop="late" :label="$t('monthAttendance.table.late')" align="center" />
  51 + <el-table-column prop="early" :label="$t('monthAttendance.table.early')" align="center" />
  52 + <el-table-column prop="noClockIn" :label="$t('monthAttendance.table.noClockIn')" align="center" />
  53 + <el-table-column prop="free" :label="$t('monthAttendance.table.free')" align="center" />
  54 + </el-table>
  55 + </div>
  56 +
  57 + <!-- 分页 -->
  58 + <el-pagination class="pagination-wrapper" :current-page="pagination.current" :page-sizes="[10, 20, 30, 50]"
  59 + :page-size="pagination.size" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
  60 + @size-change="handleSizeChange" @current-change="handleCurrentChange" />
  61 + </el-card>
  62 + </div>
  63 +</template>
  64 +
  65 +<script>
  66 +import { getMonthAttendance, getAttendanceClasses, exportMonthAttendance } from '@/api/oa/monthAttendanceManageApi'
  67 +import { getCommunityId } from '@/api/community/communityApi'
  68 +
  69 +export default {
  70 + name: 'MonthAttendanceManageList',
  71 + data() {
  72 + return {
  73 + loading: false,
  74 + searchForm: {
  75 + classesId: '',
  76 + staffName: '',
  77 + date: '',
  78 + taskYear: '',
  79 + taskMonth: ''
  80 + },
  81 + tableData: [],
  82 + attendanceClasses: [],
  83 + curDays: 30,
  84 + pagination: {
  85 + current: 1,
  86 + size: 10,
  87 + total: 0
  88 + },
  89 + communityId: ''
  90 + }
  91 + },
  92 + computed: {
  93 + tableWidth() {
  94 + const bodyWidth = document.body.clientWidth
  95 + const menuWidth = document.getElementById('menu-nav').offsetWidth || 0
  96 + return `${bodyWidth - menuWidth - 55}px`
  97 + }
  98 + },
  99 + created() {
  100 + this.communityId = getCommunityId()
  101 + this.initData()
  102 + },
  103 + methods: {
  104 + async initData() {
  105 + await this.getAttendanceClasses()
  106 + this.handleDateChange()
  107 + this.getList()
  108 + },
  109 +
  110 + async getList() {
  111 + try {
  112 + this.loading = true
  113 + const params = {
  114 + page: this.pagination.current,
  115 + row: this.pagination.size,
  116 + ...this.searchForm,
  117 + communityId: this.communityId
  118 + }
  119 + const { data, total } = await getMonthAttendance(params)
  120 + this.tableData = data
  121 + this.pagination.total = total
  122 + } catch (error) {
  123 + this.$message.error(this.$t('monthAttendance.fetchError'))
  124 + } finally {
  125 + this.loading = false
  126 + }
  127 + },
  128 +
  129 + async getAttendanceClasses() {
  130 + try {
  131 + const params = {
  132 + page: 1,
  133 + row: 100
  134 + }
  135 + const { data } = await getAttendanceClasses(params)
  136 + this.attendanceClasses = data
  137 + if (data.length > 0) {
  138 + this.searchForm.classesId = data[0].classesId
  139 + }
  140 + } catch (error) {
  141 + this.$message.error(this.$t('monthAttendance.fetchClassesError'))
  142 + }
  143 + },
  144 +
  145 + handleDateChange() {
  146 + if (this.searchForm.date) {
  147 + const [year, month] = this.searchForm.date.split('-')
  148 + this.searchForm.taskYear = year
  149 + this.searchForm.taskMonth = month
  150 + this.curDays = this.daysInMonth(year, month - 1)
  151 + } else {
  152 + const date = new Date()
  153 + this.searchForm.taskYear = date.getFullYear()
  154 + this.searchForm.taskMonth = date.getMonth() + 1
  155 + this.searchForm.date = `${date.getFullYear()}-${date.getMonth() + 1}`
  156 + this.curDays = this.daysInMonth(date.getFullYear(), date.getMonth())
  157 + }
  158 + },
  159 +
  160 + daysInMonth(year, month) {
  161 + return new Date(year, month + 1, 0).getDate()
  162 + },
  163 +
  164 + getDayDetail(detail, day) {
  165 + const currentDate = new Date()
  166 + if (
  167 + this.searchForm.taskYear == currentDate.getFullYear() &&
  168 + this.searchForm.taskMonth == (currentDate.getMonth() + 1) &&
  169 + day > currentDate.getDate()
  170 + ) {
  171 + return this.$t('monthAttendance.notTime')
  172 + }
  173 +
  174 + if (!detail || !Object.prototype.hasOwnProperty.call(detail, day)) {
  175 + return this.$t('monthAttendance.noNeed')
  176 + }
  177 +
  178 + const details = detail[day]
  179 + if (!details) {
  180 + return this.$t('monthAttendance.noNeed')
  181 + }
  182 +
  183 + let value = ''
  184 + details.forEach(element => {
  185 + if (element.specCd === '1001') {
  186 + value += `<div>${this.$t('monthAttendance.work')}:`
  187 + } else {
  188 + value += `<div>${this.$t('monthAttendance.offWork')}:`
  189 + }
  190 +
  191 + if (element.state !== '10000') {
  192 + value += `${this.timeFormat(element.checkTime)}(${element.stateName})</div>`
  193 + } else {
  194 + value += `-(${element.stateName})</div>`
  195 + }
  196 + })
  197 + return value
  198 + },
  199 +
  200 + timeFormat(time) {
  201 + if (!time) return ''
  202 + return new Date(time).toLocaleTimeString()
  203 + },
  204 +
  205 + handleSearch() {
  206 + this.pagination.current = 1
  207 + this.getList()
  208 + },
  209 +
  210 + handleReset() {
  211 + this.searchForm = {
  212 + classesId: this.attendanceClasses.length > 0 ? this.attendanceClasses[0].classesId : '',
  213 + staffName: '',
  214 + date: '',
  215 + taskYear: '',
  216 + taskMonth: ''
  217 + }
  218 + this.handleDateChange()
  219 + this.handleSearch()
  220 + },
  221 +
  222 + async handleExport() {
  223 + try {
  224 + this.loading = true
  225 + const params = {
  226 + ...this.searchForm,
  227 + communityId: this.communityId,
  228 + pagePath: 'monthAttendance'
  229 + }
  230 + await exportMonthAttendance(params)
  231 + this.$message.success(this.$t('monthAttendance.exportSuccess'))
  232 + } catch (error) {
  233 + this.$message.error(this.$t('monthAttendance.exportError'))
  234 + } finally {
  235 + this.loading = false
  236 + }
  237 + },
  238 +
  239 + handleSizeChange(val) {
  240 + this.pagination.size = val
  241 + this.getList()
  242 + },
  243 +
  244 + handleCurrentChange(val) {
  245 + this.pagination.current = val
  246 + this.getList()
  247 + }
  248 + }
  249 +}
  250 +</script>
  251 +
  252 +<style lang="scss" scoped>
  253 +.month-attendance-container {
  254 + padding: 20px;
  255 +
  256 + .search-wrapper {
  257 + margin-bottom: 20px;
  258 +
  259 + .el-form-item {
  260 + margin-bottom: 0;
  261 + }
  262 + }
  263 +
  264 + .table-wrapper {
  265 + .table-content {
  266 + overflow-x: auto;
  267 + }
  268 +
  269 + .pagination-wrapper {
  270 + margin-top: 20px;
  271 + text-align: right;
  272 + }
  273 + }
  274 +
  275 + .text-right {
  276 + text-align: right;
  277 + }
  278 +
  279 + .float-right {
  280 + float: right;
  281 + }
  282 +}
  283 +</style>
0 \ No newline at end of file 284 \ No newline at end of file
src/views/oa/newOaWorkflowDoingLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + newOaWorkflowDoing: {
  4 + myPending: 'My Pending',
  5 + myCompleted: 'My Completed',
  6 + refresh: 'Refresh',
  7 + repairPending: 'Repair Pending',
  8 + complaintPending: 'Complaint Pending',
  9 + purchasePending: 'Purchase Pending',
  10 + collectionPending: 'Collection Pending',
  11 + contractApplyPending: 'Contract Draft Pending',
  12 + contractChangePending: 'Contract Change Pending',
  13 + allocationPending: 'Allocation Pending',
  14 + itemReleasePending: 'Item Release Pending',
  15 + visitPending: 'Visit Pending',
  16 + repairCompleted: 'Repair Completed',
  17 + complaintCompleted: 'Complaint Completed',
  18 + purchaseCompleted: 'Purchase Completed',
  19 + collectionCompleted: 'Collection Completed',
  20 + contractApplyCompleted: 'Contract Draft Completed',
  21 + contractChangeCompleted: 'Contract Change Completed',
  22 + allocationCompleted: 'Allocation Completed',
  23 + itemReleaseCompleted: 'Item Release Completed',
  24 + visitCompleted: 'Visit Completed',
  25 + pending: 'Pending',
  26 + completed: 'Completed'
  27 + }
  28 + },
  29 + zh: {
  30 + newOaWorkflowDoing: {
  31 + myPending: '我的待办',
  32 + myCompleted: '我的已办',
  33 + refresh: '刷新',
  34 + repairPending: '报修待办',
  35 + complaintPending: '投诉待办',
  36 + purchasePending: '采购待办',
  37 + collectionPending: '物品领用待办',
  38 + contractApplyPending: '合同起草待办',
  39 + contractChangePending: '合同变更待办',
  40 + allocationPending: '调拨待办',
  41 + itemReleasePending: '物品放行待办',
  42 + visitPending: '访客待办',
  43 + repairCompleted: '报修已办',
  44 + complaintCompleted: '投诉已办',
  45 + purchaseCompleted: '采购已办',
  46 + collectionCompleted: '物品领用已办',
  47 + contractApplyCompleted: '合同起草已办',
  48 + contractChangeCompleted: '合同变更已办',
  49 + allocationCompleted: '调拨已办',
  50 + itemReleaseCompleted: '物品放行已办',
  51 + visitCompleted: '访客已办',
  52 + pending: '待办',
  53 + completed: '已办'
  54 + }
  55 + }
  56 +}
0 \ No newline at end of file 57 \ No newline at end of file
src/views/oa/newOaWorkflowDoingList.vue 0 → 100644
  1 +<template>
  2 + <div class="new-oa-workflow-doing-container">
  3 + <el-card class="box-card">
  4 + <div slot="header" class="flex justify-between ">
  5 + <span>{{ $t('newOaWorkflowDoing.myPending') }}</span>
  6 + <div class="header-tools">
  7 + <el-button type="primary" size="small" @click="_queryUndoOrderMethod">
  8 + <i class="el-icon-refresh"></i>
  9 + {{ $t('newOaWorkflowDoing.refresh') }}
  10 + </el-button>
  11 + </div>
  12 + </div>
  13 + <div class="card-content">
  14 + <el-row :gutter="20">
  15 + <el-col :span="4" v-for="(item, index) in pendingItems" :key="'pending-' + index">
  16 + <div class="item-wrapper text-center">
  17 + <el-link type="primary" :underline="true" @click="_toGo(item.url)">
  18 + <span>{{ $t(`newOaWorkflowDoing.${item.key}`) }}</span>
  19 + <span :class="{ 'red-text': item.count > 0 }">({{ item.count }})</span>
  20 + </el-link>
  21 + </div>
  22 + </el-col>
  23 + <el-col :span="4" v-for="(item, index) in newOaWorkflowDoingInfo.newOaWorkflows" :key="'workflow-' + index">
  24 + <div class="item-wrapper text-center">
  25 + <el-link type="primary" :underline="true" @click="_toGoA(item)">
  26 + {{ item.flowName }}
  27 + <span>{{ $t('newOaWorkflowDoing.pending') }}</span>
  28 + <span :class="{ 'red-text': item.undoCount > 0 }">({{ item.undoCount }})</span>
  29 + </el-link>
  30 + </div>
  31 + </el-col>
  32 + </el-row>
  33 + </div>
  34 + </el-card>
  35 +
  36 + <el-card class="box-card margin-top">
  37 + <div slot="header" class="flex justify-between">
  38 + <span>{{ $t('newOaWorkflowDoing.myCompleted') }}</span>
  39 + </div>
  40 + <div class="card-content">
  41 + <el-row :gutter="20">
  42 + <el-col :span="4" v-for="(item, index) in completedItems" :key="'completed-' + index">
  43 + <div class="item-wrapper text-center">
  44 + <el-link type="primary" :underline="true" @click="_toGo(item.url)">
  45 + <span>{{ $t(`newOaWorkflowDoing.${item.key}`) }}</span>
  46 + </el-link>
  47 + </div>
  48 + </el-col>
  49 + <el-col :span="4" v-for="(item, index) in newOaWorkflowDoingInfo.newOaWorkflows"
  50 + :key="'workflow-completed-' + index">
  51 + <div class="item-wrapper text-center">
  52 + <el-link type="primary" :underline="true" @click="_toGoB(item)">
  53 + {{ item.flowName }}
  54 + <span>{{ $t('newOaWorkflowDoing.completed') }}</span>
  55 + </el-link>
  56 + </div>
  57 + </el-col>
  58 + </el-row>
  59 + </div>
  60 + </el-card>
  61 + </div>
  62 +</template>
  63 +
  64 +<script>
  65 +import { getUndoInfo, listNewOaWorkflows } from '@/api/oa/newOaWorkflowDoingApi'
  66 +import { getCommunityId } from '@/api/community/communityApi'
  67 +
  68 +export default {
  69 + name: 'NewOaWorkflowDoingList',
  70 + data() {
  71 + return {
  72 + newOaWorkflowDoingInfo: {
  73 + newOaWorkflows: [],
  74 + repair: 0,
  75 + complaint: 0,
  76 + purchase: 0,
  77 + collection: 0,
  78 + contractApply: 0,
  79 + contractChange: 0,
  80 + allocation: 0,
  81 + itemReleaseCount: 0,
  82 + visitUndoCount: 0,
  83 + ownerSettledApplyCount: 0
  84 + },
  85 + pendingItems: [
  86 + { key: 'repairPending', url: '/#/pages/property/repairDispatchManage', count: 0 },
  87 + { key: 'complaintPending', url: '/#/pages/complaint/uodoComplaints', count: 0 },
  88 + { key: 'purchasePending', url: '/#/pages/admin/myAuditOrders', count: 0 },
  89 + { key: 'collectionPending', url: '/#/pages/admin/myItemOutAuditOrders', count: 0 },
  90 + { key: 'contractApplyPending', url: '/#/pages/admin/contractApplyAuditOrders', count: 0 },
  91 + { key: 'contractChangePending', url: '/#/pages/admin/contractChangeAuditOrders', count: 0 },
  92 + { key: 'allocationPending', url: '/#/pages/admin/allocationStorehouseAuditOrders', count: 0 },
  93 + { key: 'itemReleasePending', url: '/#/pages/property/itemReleaseUndo', count: 0 },
  94 + { key: 'visitPending', url: '/#/pages/property/visitUndo', count: 0 }
  95 + ],
  96 + completedItems: [
  97 + { key: 'repairCompleted', url: '/#/pages/property/myRepairDispatchManage' },
  98 + { key: 'complaintCompleted', url: '/#/pages/complaint/doHistoryComplaints' },
  99 + { key: 'purchaseCompleted', url: '/#/pages/admin/myAuditHistoryOrders' },
  100 + { key: 'collectionCompleted', url: '/#/pages/admin/myItemOutAuditHistoryOrders' },
  101 + { key: 'contractApplyCompleted', url: '/#/pages/admin/contractApplyAuditHistoryOrders' },
  102 + { key: 'contractChangeCompleted', url: '/#/pages/admin/contractChangeAuditHistoryOrders' },
  103 + { key: 'allocationCompleted', url: '/#/pages/admin/allocationStorehouseHistoryAuditOrders' },
  104 + { key: 'itemReleaseCompleted', url: '/#/pages/property/itemReleaseFinish' },
  105 + { key: 'visitCompleted', url: '/#/pages/property/visitFinish' }
  106 + ],
  107 + communityId: ''
  108 + }
  109 + },
  110 + created() {
  111 + this.communityId = getCommunityId()
  112 + this._initMethod()
  113 + },
  114 + methods: {
  115 + _initMethod() {
  116 + this._listNewOaWorkflows()
  117 + this._loadUndoInfo()
  118 + },
  119 + _toGo(url) {
  120 + this.$router.push(url)
  121 + },
  122 + _toGoA(item) {
  123 + this.$router.push(`/form.html#/pages/property/newOaWorkflow?flowId=${item.flowId}&switchValue=newOaWorkflowUndo`)
  124 + },
  125 + _toGoB(item) {
  126 + this.$router.push(`/form.html#/pages/property/newOaWorkflow?flowId=${item.flowId}&switchValue=newOaWorkflowFinish`)
  127 + },
  128 + async _listNewOaWorkflows() {
  129 + try {
  130 + const params = {
  131 + state: 'C',
  132 + page: 1,
  133 + row: 100,
  134 + flowType: '1001'
  135 + }
  136 + const { data } = await listNewOaWorkflows(params)
  137 + this.newOaWorkflowDoingInfo.newOaWorkflows = data
  138 + } catch (error) {
  139 + console.error('Failed to fetch workflows:', error)
  140 + }
  141 + },
  142 + async _loadUndoInfo() {
  143 + try {
  144 + const params = {
  145 + communityId: this.communityId,
  146 + page: 1,
  147 + row: 10
  148 + }
  149 + const data = await getUndoInfo(params)
  150 + Object.assign(this.newOaWorkflowDoingInfo, data)
  151 +
  152 + // Update counts in pendingItems
  153 + this.pendingItems[0].count = this.newOaWorkflowDoingInfo.repair
  154 + this.pendingItems[1].count = this.newOaWorkflowDoingInfo.complaint
  155 + this.pendingItems[2].count = this.newOaWorkflowDoingInfo.purchase
  156 + this.pendingItems[3].count = this.newOaWorkflowDoingInfo.collection
  157 + this.pendingItems[4].count = this.newOaWorkflowDoingInfo.contractApply
  158 + this.pendingItems[5].count = this.newOaWorkflowDoingInfo.contractChange
  159 + this.pendingItems[6].count = this.newOaWorkflowDoingInfo.allocation
  160 + this.pendingItems[7].count = this.newOaWorkflowDoingInfo.itemReleaseCount
  161 + this.pendingItems[8].count = this.newOaWorkflowDoingInfo.visitUndoCount
  162 + } catch (error) {
  163 + console.error('Failed to fetch undo info:', error)
  164 + }
  165 + },
  166 + _queryUndoOrderMethod() {
  167 + this._listNewOaWorkflows()
  168 + this._loadUndoInfo()
  169 + }
  170 + }
  171 +}
  172 +</script>
  173 +
  174 +<style lang="scss" scoped>
  175 +.new-oa-workflow-doing-container {
  176 + padding: 20px;
  177 +
  178 + .box-card {
  179 + margin-bottom: 20px;
  180 +
  181 + .header-tools {
  182 + float: right;
  183 + }
  184 +
  185 + .card-content {
  186 + padding: 10px;
  187 + }
  188 + }
  189 +
  190 + .item-wrapper {
  191 + padding: 10px 0;
  192 +
  193 + .red-text {
  194 + color: #F56C6C;
  195 + }
  196 + }
  197 +
  198 + .margin-top {
  199 + margin-top: 20px;
  200 + }
  201 +
  202 + .text-center {
  203 + text-align: center;
  204 + }
  205 +}
  206 +</style>
0 \ No newline at end of file 207 \ No newline at end of file
src/views/oa/newOaWorkflowManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + newOaWorkflowManage: {
  4 + noPublishedFlow: 'You have not published any workflows, please publish them in the workflow instances first',
  5 + flowNotDeployed: 'Workflow is not deployed'
  6 + }
  7 + },
  8 + zh: {
  9 + newOaWorkflowManage: {
  10 + noPublishedFlow: '您还没有发布流程,请先到流程实例中发布流程',
  11 + flowNotDeployed: '流程未部署'
  12 + }
  13 + }
  14 +}
0 \ No newline at end of file 15 \ No newline at end of file
src/views/oa/newOaWorkflowManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="new-oa-workflow-manage-container">
  3 + <el-card class="box-card">
  4 + <div v-if="newOaWorkflowManageInfo.newOaWorkflows && newOaWorkflowManageInfo.newOaWorkflows.length > 0">
  5 + <el-row :gutter="20">
  6 + <el-col
  7 + v-for="(item, index) in newOaWorkflowManageInfo.newOaWorkflows"
  8 + :key="index"
  9 + :span="4"
  10 + class="text-center workflow-item"
  11 + @click.native="newFlow(item)"
  12 + >
  13 + <div>
  14 + <img src="/img/flow.png" width="80px" />
  15 + </div>
  16 + <div class="margin-top">
  17 + <span style="color: #333333;">{{ item.flowName }}</span>
  18 + </div>
  19 + </el-col>
  20 + </el-row>
  21 + </div>
  22 + <div v-else class="empty-tip">
  23 + <span>{{ $t('newOaWorkflowManage.noPublishedFlow') }}</span>
  24 + </div>
  25 + </el-card>
  26 + </div>
  27 +</template>
  28 +
  29 +<script>
  30 +import { queryOaWorkflow } from '@/api/oa/newOaWorkflowManageApi'
  31 +
  32 +export default {
  33 + name: 'NewOaWorkflowManageList',
  34 + data() {
  35 + return {
  36 + newOaWorkflowManageInfo: {
  37 + newOaWorkflows: [],
  38 + conditions: {
  39 + state: 'C',
  40 + flowType: '1001',
  41 + page: 1,
  42 + row: 100
  43 + }
  44 + }
  45 + }
  46 + },
  47 + created() {
  48 + this._listNewOaWorkflows()
  49 + },
  50 + methods: {
  51 + async _listNewOaWorkflows() {
  52 + try {
  53 + const { data } = await queryOaWorkflow(this.newOaWorkflowManageInfo.conditions)
  54 + this.newOaWorkflowManageInfo.newOaWorkflows = data
  55 + } catch (error) {
  56 + console.error('Failed to fetch workflows:', error)
  57 + }
  58 + },
  59 + async newFlow(flow) {
  60 + try {
  61 + const { data } = await queryOaWorkflow({
  62 + page: 1,
  63 + row: 1,
  64 + flowId: flow.flowId,
  65 + state: 'C'
  66 + })
  67 +
  68 + if (data.length < 1) {
  69 + this.$message.error(this.$t('newOaWorkflowManage.flowNotDeployed'))
  70 + return
  71 + }
  72 +
  73 + this.$router.push({
  74 + path: '/form.html#/pages/property/newOaWorkflow',
  75 + query: { flowId: flow.flowId }
  76 + })
  77 + } catch (error) {
  78 + console.error('Failed to check flow deployment:', error)
  79 + }
  80 + }
  81 + }
  82 +}
  83 +</script>
  84 +
  85 +<style lang="scss" scoped>
  86 +.new-oa-workflow-manage-container {
  87 + padding: 20px;
  88 +
  89 + .box-card {
  90 + min-height: 400px;
  91 + }
  92 +
  93 + .workflow-item {
  94 + margin-bottom: 20px;
  95 + cursor: pointer;
  96 + transition: all 0.3s;
  97 +
  98 + &:hover {
  99 + transform: translateY(-5px);
  100 + }
  101 + }
  102 +
  103 + .empty-tip {
  104 + padding: 20px;
  105 + text-align: center;
  106 + color: #909399;
  107 + }
  108 +
  109 + .margin-top {
  110 + margin-top: 10px;
  111 + }
  112 +}
  113 +</style>
0 \ No newline at end of file 114 \ No newline at end of file
src/views/oa/staffAttendanceManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + staffAttendance: {
  4 + monthPlaceholder: 'Select month',
  5 + orgPlaceholder: 'Select organization',
  6 + staffPlaceholder: 'Staff name',
  7 + replenish: 'Replenish',
  8 + work: 'Work',
  9 + offWork: 'Off work',
  10 + log: 'Attendance log',
  11 + notTime: 'Not time yet',
  12 + noNeed: 'No need',
  13 + noTask: 'No replenish task',
  14 + chooseOrg: 'Select Organization',
  15 + loadOrgFailed: 'Failed to load organizations',
  16 + selectOrgFirst: 'Please select an organization first',
  17 + attendanceDetail: 'Attendance Details',
  18 + staffName: 'Staff Name',
  19 + face: 'Face',
  20 + clockTime: 'Clock Time',
  21 + loadDetailFailed: 'Failed to load attendance details',
  22 + replenishCheckIn: 'Replenish Attendance',
  23 + attendanceTask: 'Attendance Task',
  24 + selectTask: 'Please select task',
  25 + selectTaskFirst: 'Please select a task first',
  26 + reason: 'Reason',
  27 + inputReason: 'Please input reason',
  28 + inputReasonFirst: 'Please input reason first',
  29 + submitReplenish: 'Submit Replenish',
  30 + replenishSuccess: 'Replenish successfully',
  31 + replenishFailed: 'Replenish failed'
  32 + }
  33 + },
  34 + zh: {
  35 + staffAttendance: {
  36 + monthPlaceholder: '请选择月份',
  37 + orgPlaceholder: '请选择组织',
  38 + staffPlaceholder: '员工名称',
  39 + replenish: '补考勤',
  40 + work: '上班',
  41 + offWork: '下班',
  42 + log: '考勤记录',
  43 + notTime: '未到时间',
  44 + noNeed: '无需考勤',
  45 + noTask: '不存在补考勤任务',
  46 + chooseOrg: '选择组织',
  47 + loadOrgFailed: '加载组织失败',
  48 + selectOrgFirst: '请先选择组织',
  49 + attendanceDetail: '考勤明细',
  50 + staffName: '员工名称',
  51 + face: '人脸',
  52 + clockTime: '考勤时间',
  53 + loadDetailFailed: '加载考勤明细失败',
  54 + replenishCheckIn: '补考勤',
  55 + attendanceTask: '考勤任务',
  56 + selectTask: '请选择考勤任务',
  57 + selectTaskFirst: '请先选择考勤任务',
  58 + reason: '原因',
  59 + inputReason: '请填写原因',
  60 + inputReasonFirst: '请先填写原因',
  61 + submitReplenish: '提交补考勤',
  62 + replenishSuccess: '补考勤成功',
  63 + replenishFailed: '补考勤失败'
  64 + }
  65 + }
  66 +}
0 \ No newline at end of file 67 \ No newline at end of file
src/views/oa/staffAttendanceManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="staff-attendance-manage-container">
  3 + <el-row :gutter="20">
  4 + <el-col :span="4">
  5 + <el-card class="box-card">
  6 + <div class="filter-wrapper">
  7 + <el-date-picker v-model="staffAttendanceManageInfo.curDate" type="month"
  8 + :placeholder="$t('staffAttendance.monthPlaceholder')" value-format="yyyy-MM" class="filter-item"
  9 + @change="handleDateChange"></el-date-picker>
  10 +
  11 + <el-input v-model="staffAttendanceManageInfo.orgName" readonly
  12 + :placeholder="$t('staffAttendance.orgPlaceholder')" class="filter-item"
  13 + @focus="handleOrgFocus"></el-input>
  14 +
  15 + <el-input v-model="staffAttendanceManageInfo.staffNameLike"
  16 + :placeholder="$t('staffAttendance.staffPlaceholder')" class="filter-item"
  17 + @keyup.enter.native="loadStaffs"></el-input>
  18 +
  19 + <el-button type="primary" class="filter-item" @click="loadStaffs">
  20 + {{ $t('common.search') }}
  21 + </el-button>
  22 + </div>
  23 +
  24 + <div class="staff-list">
  25 + <ul class="staff-ul">
  26 + <li v-for="(item, index) in staffAttendanceManageInfo.staffs" :key="index" class="staff-item"
  27 + :class="{ 'active': staffAttendanceManageInfo.curStaffId === item.userId }" @click="switchStaff(item)">
  28 + {{ item.name }}
  29 + </li>
  30 + </ul>
  31 + </div>
  32 + </el-card>
  33 + </el-col>
  34 +
  35 + <el-col :span="20">
  36 + <el-card class="box-card">
  37 + <div class="attendance-grid">
  38 + <el-row :gutter="20">
  39 + <el-col v-for="index in staffAttendanceManageInfo.maxDay" :key="index" :span="4" class="attendance-day"
  40 + :style="{ backgroundColor: getBgColor(index) }">
  41 + <div class="day-header">
  42 + {{ staffAttendanceManageInfo.curYear }}-{{ staffAttendanceManageInfo.curMonth }}-{{ index }}
  43 + </div>
  44 +
  45 + <div v-if="getDayAttendance(index) && getDayAttendance(index).state !== '30000'" class="replenish-btn"
  46 + @click="replenishCheckIn(index)">
  47 + {{ $t('staffAttendance.replenish') }}
  48 + </div>
  49 +
  50 + <div v-for="(item, detailIndex) in getAttendanceDetail(index)" :key="detailIndex"
  51 + class="attendance-detail">
  52 + <div v-if="item.rest">{{ item.rest }}</div>
  53 + <div v-else>
  54 + {{ item.specCd === '1001' ? $t('staffAttendance.work') : $t('staffAttendance.offWork') }}:
  55 + <span v-if="item.state !== '10000'">{{ formatTime(item.checkTime) }}</span>
  56 + <span v-else> - </span>
  57 + <span>({{ item.stateName }})</span>
  58 + </div>
  59 + </div>
  60 +
  61 + <div class="log-link">
  62 + <el-link type="primary" @click="checkInLog(index)">
  63 + {{ $t('staffAttendance.log') }}
  64 + </el-link>
  65 + </div>
  66 + </el-col>
  67 + </el-row>
  68 + </div>
  69 + </el-card>
  70 + </el-col>
  71 + </el-row>
  72 +
  73 + <!-- 子组件 -->
  74 + <choose-org-tree ref="chooseOrgTree" @switchOrg="handleSwitchOrg"></choose-org-tree>
  75 + <staff-attendance-detail ref="staffAttendanceDetail"></staff-attendance-detail>
  76 + <staff-attendance-replenish-check-in ref="staffAttendanceReplenishCheckIn"></staff-attendance-replenish-check-in>
  77 + </div>
  78 +</template>
  79 +
  80 +<script>
  81 +import { getCommunityId } from '@/api/community/communityApi'
  82 +import {
  83 + queryStaffInfos,
  84 + queryAttendanceClassesTask,
  85 +
  86 +} from '@/api/oa/staffAttendanceManageApi'
  87 +
  88 +export default {
  89 + name: 'StaffAttendanceManageList',
  90 + components: {
  91 + 'choose-org-tree': () => import('@/components/oa/chooseOrgTree'),
  92 + 'staff-attendance-detail': () => import('@/components/oa/staffAttendanceDetail'),
  93 + 'staff-attendance-replenish-check-in': () => import('@/components/oa/staffAttendanceReplenishCheckIn')
  94 + },
  95 + data() {
  96 + return {
  97 + communityId: '',
  98 + staffAttendanceManageInfo: {
  99 + staffs: [],
  100 + attendances: [],
  101 + classesId: '',
  102 + orgId: '',
  103 + orgName: '',
  104 + curDate: '',
  105 + curYear: '',
  106 + curMonth: '',
  107 + curStaffId: '',
  108 + maxDay: 31,
  109 + staffNameLike: ''
  110 + }
  111 + }
  112 + },
  113 + created() {
  114 + this.communityId = getCommunityId()
  115 + this.initStaffDate()
  116 + this.loadStaffs()
  117 + },
  118 + methods: {
  119 + initStaffDate() {
  120 + const date = new Date()
  121 + this.staffAttendanceManageInfo.curMonth = date.getMonth() + 1
  122 + this.staffAttendanceManageInfo.curYear = date.getFullYear()
  123 + this.staffAttendanceManageInfo.curDate = `${date.getFullYear()}-${date.getMonth() + 1}`
  124 + this.staffAttendanceManageInfo.maxDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
  125 + },
  126 +
  127 + handleDateChange(val) {
  128 + if (val) {
  129 + const values = val.split('-')
  130 + this.staffAttendanceManageInfo.curYear = values[0]
  131 + this.staffAttendanceManageInfo.curMonth = values[1]
  132 + this.staffAttendanceManageInfo.maxDay = new Date(values[0], values[1], 0).getDate()
  133 + this.loadStaffAttendances()
  134 + }
  135 + },
  136 +
  137 + async loadStaffs() {
  138 + try {
  139 + const params = {
  140 + page: 1,
  141 + row: 100,
  142 + orgId: this.staffAttendanceManageInfo.orgId,
  143 + staffName: this.staffAttendanceManageInfo.staffNameLike,
  144 + communityId: this.communityId
  145 + }
  146 +
  147 + const { data } = await queryStaffInfos(params)
  148 + this.staffAttendanceManageInfo.staffs = data.staffs || []
  149 +
  150 + if (this.staffAttendanceManageInfo.staffs.length > 0) {
  151 + this.staffAttendanceManageInfo.curStaffId = this.staffAttendanceManageInfo.staffs[0].userId
  152 + this.loadStaffAttendances()
  153 + }
  154 + } catch (error) {
  155 + console.error('Failed to load staffs:', error)
  156 + }
  157 + },
  158 +
  159 + async loadStaffAttendances() {
  160 + if (!this.staffAttendanceManageInfo.curStaffId || !this.staffAttendanceManageInfo.curDate) return
  161 +
  162 + try {
  163 + const params = {
  164 + page: 1,
  165 + row: 1000,
  166 + date: this.staffAttendanceManageInfo.curDate,
  167 + staffId: this.staffAttendanceManageInfo.curStaffId,
  168 + communityId: this.communityId
  169 + }
  170 +
  171 + const { data } = await queryAttendanceClassesTask(params)
  172 + this.staffAttendanceManageInfo.attendances = data || []
  173 + } catch (error) {
  174 + console.error('Failed to load staff attendances:', error)
  175 + }
  176 + },
  177 +
  178 + handleOrgFocus() {
  179 + this.$refs.chooseOrgTree.open()
  180 + },
  181 +
  182 + handleSwitchOrg(org) {
  183 + this.staffAttendanceManageInfo.orgId = org.orgId
  184 + this.staffAttendanceManageInfo.orgName = org.allOrgName
  185 + this.loadStaffs()
  186 + },
  187 +
  188 + switchStaff(staff) {
  189 + this.staffAttendanceManageInfo.curStaffId = staff.userId
  190 + this.loadStaffAttendances()
  191 + },
  192 +
  193 + getDayAttendance(day) {
  194 + if (!this.staffAttendanceManageInfo.attendances) return null
  195 +
  196 + return this.staffAttendanceManageInfo.attendances.find(item => item.taskDay === day) || null
  197 + },
  198 +
  199 + getAttendanceDetail(day) {
  200 + const attendance = this.getDayAttendance(day)
  201 +
  202 + if (!attendance) {
  203 + const date = new Date()
  204 + const taskYear = this.staffAttendanceManageInfo.curYear
  205 + const taskMonth = this.staffAttendanceManageInfo.curMonth
  206 +
  207 + if (taskYear == date.getFullYear() && parseInt(taskMonth) == (date.getMonth() + 1) && day > date.getDate()) {
  208 + return [{ rest: this.$t('staffAttendance.notTime') }]
  209 + }
  210 +
  211 + return [{ rest: this.$t('staffAttendance.noNeed') }]
  212 + }
  213 +
  214 + return attendance.attendanceClassesTaskDetails || []
  215 + },
  216 +
  217 + getBgColor(day) {
  218 + console.log(day)
  219 + return '#fff'
  220 + },
  221 +
  222 + formatTime(time) {
  223 + if (!time) return ''
  224 + return this.$moment(time).format('HH:mm:ss')
  225 + },
  226 +
  227 + checkInLog(day) {
  228 + let month = this.staffAttendanceManageInfo.curMonth
  229 + if (month < 10) month = `0${month}`
  230 + if (day < 10) day = `0${day}`
  231 +
  232 + const date = `${this.staffAttendanceManageInfo.curYear}-${month}-${day}`
  233 + this.$refs.staffAttendanceDetail.open({
  234 + staffId: this.staffAttendanceManageInfo.curStaffId,
  235 + date: date
  236 + })
  237 + },
  238 +
  239 + replenishCheckIn(day) {
  240 + const details = this.getAttendanceDetail(day)
  241 + if (!details || details.length < 1) {
  242 + this.$message.warning(this.$t('staffAttendance.noTask'))
  243 + return
  244 + }
  245 +
  246 + const newDetails = details.filter(item => item.state === '10000')
  247 + if (newDetails.length < 1) {
  248 + this.$message.warning(this.$t('staffAttendance.noTask'))
  249 + return
  250 + }
  251 +
  252 + this.$refs.staffAttendanceReplenishCheckIn.open(newDetails)
  253 + }
  254 + }
  255 +}
  256 +</script>
  257 +
  258 +<style lang="scss" scoped>
  259 +.staff-attendance-manage-container {
  260 + padding: 20px;
  261 +
  262 + .box-card {
  263 + margin-bottom: 20px;
  264 + }
  265 +
  266 + .filter-wrapper {
  267 + .filter-item {
  268 + margin-bottom: 15px;
  269 + width: 100%;
  270 + }
  271 + }
  272 +
  273 + .staff-list {
  274 + max-height: 600px;
  275 + overflow-y: auto;
  276 +
  277 + .staff-ul {
  278 + list-style: none;
  279 + padding: 0;
  280 + margin: 0;
  281 +
  282 + .staff-item {
  283 + padding: 10px;
  284 + margin-bottom: 5px;
  285 + cursor: pointer;
  286 + border-radius: 4px;
  287 + text-align: center;
  288 +
  289 + &:hover {
  290 + background-color: #f5f7fa;
  291 + }
  292 +
  293 + &.active {
  294 + background-color: #409eff;
  295 + color: #fff;
  296 + }
  297 + }
  298 + }
  299 + }
  300 +
  301 + .attendance-grid {
  302 + .attendance-day {
  303 + padding: 10px;
  304 + margin-bottom: 20px;
  305 + border-radius: 4px;
  306 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  307 +
  308 + .day-header {
  309 + font-weight: bold;
  310 + margin-bottom: 10px;
  311 + text-align: center;
  312 + }
  313 +
  314 + .replenish-btn {
  315 + color: #409eff;
  316 + cursor: pointer;
  317 + text-align: center;
  318 + margin-bottom: 10px;
  319 +
  320 + &:hover {
  321 + text-decoration: underline;
  322 + }
  323 + }
  324 +
  325 + .attendance-detail {
  326 + margin-bottom: 5px;
  327 + font-size: 12px;
  328 + }
  329 +
  330 + .log-link {
  331 + text-align: center;
  332 + margin-top: 10px;
  333 + }
  334 + }
  335 + }
  336 +}
  337 +</style>
0 \ No newline at end of file 338 \ No newline at end of file
src/views/oa/todayAttendanceManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + todayAttendanceManage: {
  4 + search: {
  5 + title: 'Search Conditions',
  6 + classesName: 'Please enter team name',
  7 + departmentName: 'Please enter department name',
  8 + date: 'Please select attendance date',
  9 + staffName: 'Please enter staff name'
  10 + },
  11 + list: {
  12 + title: 'Today Attendance'
  13 + },
  14 + table: {
  15 + staffName: 'Staff Name',
  16 + classesName: 'Attendance Team',
  17 + attendanceDate: 'Attendance Date',
  18 + stateName: 'Attendance Status',
  19 + operation: 'Operation'
  20 + },
  21 + fetchError: 'Failed to fetch attendance data'
  22 + },
  23 + todayAttendanceDetail: {
  24 + title: 'Attendance Details',
  25 + table: {
  26 + name: 'Name',
  27 + work: 'Work',
  28 + offWork: 'Off Work',
  29 + attendanceRange: 'Attendance Range',
  30 + lateEarly: 'Late/Early',
  31 + status: 'Status',
  32 + attendanceTime: 'Attendance Time',
  33 + notCheckIn: 'Not Checked In',
  34 + snapshot: 'Snapshot'
  35 + }
  36 + }
  37 + },
  38 + zh: {
  39 + todayAttendanceManage: {
  40 + search: {
  41 + title: '查询条件',
  42 + classesName: '请输入班组名称',
  43 + departmentName: '请输入部门名称',
  44 + date: '请选择打卡时间',
  45 + staffName: '请输入员工名称'
  46 + },
  47 + list: {
  48 + title: '今日考勤'
  49 + },
  50 + table: {
  51 + staffName: '员工名称',
  52 + classesName: '考勤班组',
  53 + attendanceDate: '考勤时间',
  54 + stateName: '考勤状态',
  55 + operation: '操作'
  56 + },
  57 + fetchError: '获取考勤数据失败'
  58 + },
  59 + todayAttendanceDetail: {
  60 + title: '考勤详情',
  61 + table: {
  62 + name: '名称',
  63 + work: '上班',
  64 + offWork: '下班',
  65 + attendanceRange: '打卡范围',
  66 + lateEarly: '迟到/早退',
  67 + status: '状态',
  68 + attendanceTime: '打卡时间',
  69 + notCheckIn: '未打卡',
  70 + snapshot: '抓拍'
  71 + }
  72 + }
  73 + }
  74 +}
0 \ No newline at end of file 75 \ No newline at end of file
src/views/oa/todayAttendanceManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="today-attendance-container">
  3 + <!-- 查询条件 -->
  4 + <el-card class="search-wrapper">
  5 + <div slot="header" class="flex justify-between">
  6 + <span>{{ $t('todayAttendanceManage.search.title') }}</span>
  7 + </div>
  8 + <el-row :gutter="20">
  9 + <el-col :span="4">
  10 + <el-input
  11 + v-model="searchForm.classesName"
  12 + :placeholder="$t('todayAttendanceManage.search.classesName')"
  13 + clearable
  14 + @keyup.enter.native="handleSearch"
  15 + />
  16 + </el-col>
  17 + <el-col :span="4">
  18 + <el-input
  19 + v-model="searchForm.departmentName"
  20 + :placeholder="$t('todayAttendanceManage.search.departmentName')"
  21 + clearable
  22 + @keyup.enter.native="handleSearch"
  23 + />
  24 + </el-col>
  25 + <el-col :span="4">
  26 + <el-date-picker
  27 + v-model="searchForm.date"
  28 + type="date"
  29 + :placeholder="$t('todayAttendanceManage.search.date')"
  30 + value-format="yyyy-MM-dd"
  31 + style="width: 100%"
  32 + />
  33 + </el-col>
  34 + <el-col :span="4">
  35 + <el-input
  36 + v-model="searchForm.staffName"
  37 + :placeholder="$t('todayAttendanceManage.search.staffName')"
  38 + clearable
  39 + @keyup.enter.native="handleSearch"
  40 + />
  41 + </el-col>
  42 + <el-col :span="4">
  43 + <el-button type="primary" @click="handleSearch">{{ $t('common.search') }}</el-button>
  44 + <el-button @click="handleReset">{{ $t('common.reset') }}</el-button>
  45 + </el-col>
  46 + </el-row>
  47 + </el-card>
  48 +
  49 + <!-- 今日考勤列表 -->
  50 + <el-card class="list-wrapper">
  51 + <div slot="header" class="flex justify-between">
  52 + <span>{{ $t('todayAttendanceManage.list.title') }}</span>
  53 + </div>
  54 + <el-table
  55 + v-loading="loading"
  56 + :data="tableData"
  57 + border
  58 + style="width: 100%"
  59 + >
  60 + <el-table-column
  61 + prop="staffName"
  62 + :label="$t('todayAttendanceManage.table.staffName')"
  63 + align="center"
  64 + />
  65 + <el-table-column
  66 + prop="classesName"
  67 + :label="$t('todayAttendanceManage.table.classesName')"
  68 + align="center"
  69 + />
  70 + <el-table-column
  71 + :label="$t('todayAttendanceManage.table.attendanceDate')"
  72 + align="center"
  73 + >
  74 + <template slot-scope="scope">
  75 + {{ `${scope.row.taskYear}-${scope.row.taskMonth}-${scope.row.taskDay}` }}
  76 + </template>
  77 + </el-table-column>
  78 + <el-table-column
  79 + prop="stateName"
  80 + :label="$t('todayAttendanceManage.table.stateName')"
  81 + align="center"
  82 + />
  83 + <el-table-column
  84 + :label="$t('common.operation')"
  85 + align="center"
  86 + width="120"
  87 + >
  88 + <template slot-scope="scope">
  89 + <el-button
  90 + size="mini"
  91 + type="primary"
  92 + @click="handleDetail(scope.row)"
  93 + >
  94 + {{ $t('common.detail') }}
  95 + </el-button>
  96 + </template>
  97 + </el-table-column>
  98 + </el-table>
  99 +
  100 + <el-pagination
  101 + :current-page.sync="page.current"
  102 + :page-sizes="[10, 20, 30, 50]"
  103 + :page-size="page.size"
  104 + :total="page.total"
  105 + layout="total, sizes, prev, pager, next, jumper"
  106 + @size-change="handleSizeChange"
  107 + @current-change="handleCurrentChange"
  108 + />
  109 + </el-card>
  110 +
  111 + <!-- 考勤详情弹窗 -->
  112 + <today-attendance-detail ref="detailDialog" />
  113 + </div>
  114 +</template>
  115 +
  116 +<script>
  117 +import { queryAttendanceClassesTask } from '@/api/oa/todayAttendanceManageApi'
  118 +import TodayAttendanceDetail from '@/components/oa/todayAttendanceDetail'
  119 +
  120 +export default {
  121 + name: 'TodayAttendanceManageList',
  122 + components: {
  123 + TodayAttendanceDetail
  124 + },
  125 + data() {
  126 + return {
  127 + loading: false,
  128 + searchForm: {
  129 + classesName: '',
  130 + departmentName: '',
  131 + date: '',
  132 + staffName: ''
  133 + },
  134 + tableData: [],
  135 + page: {
  136 + current: 1,
  137 + size: 10,
  138 + total: 0
  139 + }
  140 + }
  141 + },
  142 + created() {
  143 + this.getList()
  144 + },
  145 + methods: {
  146 + async getList() {
  147 + try {
  148 + this.loading = true
  149 + const params = {
  150 + page: this.page.current,
  151 + row: this.page.size,
  152 + ...this.searchForm
  153 + }
  154 + const { data, total } = await queryAttendanceClassesTask(params)
  155 + this.tableData = data
  156 + this.page.total = total
  157 + } catch (error) {
  158 + this.$message.error(this.$t('todayAttendanceManage.fetchError'))
  159 + } finally {
  160 + this.loading = false
  161 + }
  162 + },
  163 + handleSearch() {
  164 + this.page.current = 1
  165 + this.getList()
  166 + },
  167 + handleReset() {
  168 + this.searchForm = {
  169 + classesName: '',
  170 + departmentName: '',
  171 + date: '',
  172 + staffName: ''
  173 + }
  174 + this.page.current = 1
  175 + this.getList()
  176 + },
  177 + handleDetail(row) {
  178 + this.$refs.detailDialog.open(row)
  179 + },
  180 + handleSizeChange(val) {
  181 + this.page.size = val
  182 + this.getList()
  183 + },
  184 + handleCurrentChange(val) {
  185 + this.page.current = val
  186 + this.getList()
  187 + }
  188 + }
  189 +}
  190 +</script>
  191 +
  192 +<style lang="scss" scoped>
  193 +.today-attendance-container {
  194 + padding: 20px;
  195 +
  196 + .search-wrapper {
  197 + margin-bottom: 20px;
  198 +
  199 + .el-col {
  200 + margin-bottom: 10px;
  201 + }
  202 + }
  203 +
  204 + .list-wrapper {
  205 + .el-pagination {
  206 + margin-top: 20px;
  207 + text-align: right;
  208 + }
  209 + }
  210 +}
  211 +</style>
0 \ No newline at end of file 212 \ No newline at end of file