Просмотр исходного кода

feat: 更新国际化语言文件,新增考勤相关错误提示信息和确认信息,优化用户在移动端的打卡体验。

master
lizhuang 2 дней назад
Родитель
Сommit
c6897574c4
5 измененных файлов: 165 добавлений и 135 удалений
  1. 3
    0
      src/i18n/lang/en.js
  2. 3
    0
      src/i18n/lang/ja.js
  3. 4
    1
      src/i18n/lang/zh.js
  4. 1
    0
      src/router/index.js
  5. 154
    134
      src/views/oa/attendance/checkin/index.vue

+ 3
- 0
src/i18n/lang/en.js Просмотреть файл

@@ -209,6 +209,9 @@ export default {
error: {
outOfRange: "Please check in within the range",
submitFailed: "Submit failed",
pcMessage: "Please use the mobile phone to check in",
message: "Message",
confirm: "Confirm",
},
},


+ 3
- 0
src/i18n/lang/ja.js Просмотреть файл

@@ -210,6 +210,9 @@ export default {
error: {
outOfRange: "打刻範囲外です。打刻範囲内に入ってから打刻してください。",
submitFailed: "送信失敗",
pcMessage: "携帯電話で考勤打刻を行ってください",
message: "メッセージ",
confirm: "確認",
},
},


+ 4
- 1
src/i18n/lang/zh.js Просмотреть файл

@@ -142,7 +142,7 @@ export default {
title: "500",
message: "抱歉,服务器内部错误",
back: "返回首页",
},
}
},

login: {
@@ -209,6 +209,9 @@ export default {
error: {
outOfRange: "请在打卡范围内进行打卡",
submitFailed: "提交失败",
pcMessage: "请使用手机端,进行考勤打卡",
message: "提示",
confirm: "确定",
},
},


+ 1
- 0
src/router/index.js Просмотреть файл

@@ -92,6 +92,7 @@ export const constantRoutes = [
{
path: "/m/checkin",
component: () => import("@/views/oa/attendance/checkin"),
hidden: true,
meta: { title: "menu.checkin", icon: "date" },
},
];

+ 154
- 134
src/views/oa/attendance/checkin/index.vue Просмотреть файл

@@ -38,22 +38,22 @@
<div class="status-card">
<div class="status-time">
<!-- 上班 -->
{{ $t("checkin.workStartTime") }}
{{ $t('checkin.workStartTime') }}
{{ attendanceGroup.workStartTime }}
</div>
<div class="status-label">
<span>{{ getCheckInStatusText("clockIn") }}</span>
<span>{{ getCheckInStatusText('clockIn') }}</span>
<span>{{ formatTime(attendanceStatus.clockInTime) }}</span>
</div>
</div>
<div class="status-card">
<div class="status-time">
<!-- 下班 -->
{{ $t("checkin.workEndTime") }}
{{ $t('checkin.workEndTime') }}
{{ attendanceGroup.workEndTime }}
</div>
<div class="status-label">
<span>{{ getCheckInStatusText("clockOut") }}</span>
<span>{{ getCheckInStatusText('clockOut') }}</span>
<span>{{ formatTime(attendanceStatus.clockOutTime) }}</span>
</div>
</div>
@@ -122,35 +122,38 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { AttendanceService, PUNCH_STATUS, PUNCH_TYPE } from "./service";
import RemarkDialog from "./components/RemarkDialog.vue";
import SuccessDialog from "./components/SuccessDialog.vue";
import LocationInfo from "./components/LocationInfo.vue";
import { getGeocode } from "@/api/oa/attendance/clockIn";
import moment from "moment";
import timeService from "@/utils/timeService";
import { mapGetters } from 'vuex'
import { AttendanceService, PUNCH_STATUS, PUNCH_TYPE } from './service'
import RemarkDialog from './components/RemarkDialog.vue'
import SuccessDialog from './components/SuccessDialog.vue'
import LocationInfo from './components/LocationInfo.vue'
import { getGeocode } from '@/api/oa/attendance/clockIn'
import moment from 'moment'
import timeService from '@/utils/timeService'
import ResizeMixin from '@/layout/mixin/ResizeHandler'

export default {
name: "MCheckin",
name: 'MCheckin',
components: {
RemarkDialog,
SuccessDialog,
LocationInfo,
LocationInfo
},
mixins: [ResizeMixin],
data() {
return {
// 时间相关
currentTime: "",
currentCompleteDate: "",
currentTime: '',
currentCompleteDate: '',
removeTimeUpdateCallback: null,

// 打卡成功地址
formattedAddress: "",
formattedAddress: '',

// 位置相关
userLocation: {
latitude: null,
longitude: null,
longitude: null
},
isInRange: false,
watchId: null,
@@ -161,43 +164,44 @@ export default {
clockInTime: null,
clockOutTime: null,
clockInStatus: PUNCH_STATUS.NOT_CHECKED,
clockOutStatus: PUNCH_STATUS.NOT_CHECKED,
clockOutStatus: PUNCH_STATUS.NOT_CHECKED
},

// 弹窗状态
showRemarkDialog: false,
showSuccessDialog: false,
remarkDialogTitle: "",
remarkDialogTitle: '',
remarkForm: {
remark: "",
type: "",
remark: '',
type: ''
},
};
isPC: false
}
},

computed: {
...mapGetters(["userinfo", "nickname"]),
...mapGetters(['userinfo', 'nickname']),

hasBirthday() {
if (!this.userinfo.birthday) return false;
if (!this.userinfo.birthday) return false
// 使用服务器时间组件而不是系统时间
const timeComponents = timeService.getCurrentTimeComponents();
const birthday = this.userinfo.birthday.split("-");
const timeComponents = timeService.getCurrentTimeComponents()
const birthday = this.userinfo.birthday.split('-')

const month = birthday[1];
const day = birthday[2];
const month = birthday[1]
const day = birthday[2]

const todayMonth = String(timeComponents.month).padStart(2, "0");
const todayDay = String(timeComponents.day).padStart(2, "0");
const todayMonth = String(timeComponents.month).padStart(2, '0')
const todayDay = String(timeComponents.day).padStart(2, '0')

return month == todayMonth && day == todayDay;
return month == todayMonth && day == todayDay
},
birthdayMessage() {
if (!this.userinfo.birthday) return "";
return this.$t("home.birthday", {
if (!this.userinfo.birthday) return ''
return this.$t('home.birthday', {
name: this.nickname,
birthday: moment(this.userinfo.birthday).format(`M/D`),
});
birthday: moment(this.userinfo.birthday).format(`M/D`)
})
},

/**
@@ -208,7 +212,7 @@ export default {
return (
this.attendanceGroup.timeZone ||
Intl.DateTimeFormat().resolvedOptions().timeZone
);
)
},

/**
@@ -216,13 +220,13 @@ export default {
* @returns {string} 按钮文本
*/
punchButtonText() {
const { clockInStatus, clockOutStatus } = this.attendanceStatus;
const { clockInStatus, clockOutStatus } = this.attendanceStatus

console.log(PUNCH_STATUS);
console.log(PUNCH_STATUS)

if (clockInStatus == PUNCH_STATUS.NOT_CHECKED) {
// return '上班打卡'
return this.$t("checkin.button.notChecked");
return this.$t('checkin.button.notChecked')
}
if (
clockOutStatus == PUNCH_STATUS.NOT_CHECKED ||
@@ -230,27 +234,43 @@ export default {
clockOutStatus == PUNCH_STATUS.LATE_IN
) {
// return '下班打卡'
return this.$t("checkin.button.checkedIn");
return this.$t('checkin.button.checkedIn')
}
// return '已打卡'
return this.$t("checkin.button.checkedOut");
},
return this.$t('checkin.button.checkedOut')
}
},

async mounted() {
try {
if (this.userinfo.language) {
this.$i18n.locale = this.userinfo.language;
this.$store.dispatch("language/setLanguage", this.userinfo.language);
this.$i18n.locale = this.userinfo.language
this.$store.dispatch('language/setLanguage', this.userinfo.language)
}
// 判断如果是PC端
if (!this.$_isMobile()) {
this.$alert(this.$t('checkin.error.pcMessage'), this.$t('checkin.error.message'), {
confirmButtonText: this.$t('checkin.error.confirm'),
showCancelButton: false,
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
center: true,
callback: () => {
this.$router.push('/')
}
})
return
} else {
await this.initializeApp()
}
await this.initializeApp();
} catch (error) {
// this.handleError(this.$i18n.t("checkin.error.init"), error);
}
},

beforeDestroy() {
this.cleanup();
this.cleanup()
},

methods: {
@@ -260,38 +280,38 @@ export default {
async initializeApp() {
// 初始化时间服务(获取服务器时间)
try {
const syncSuccess = await timeService.initialize();
const syncSuccess = await timeService.initialize()
if (!syncSuccess) {
console.warn("服务器时间同步失败,将使用设备本地时间");
console.warn('服务器时间同步失败,将使用设备本地时间')
// 可以选择性地显示警告提示
// this.$toast.warning('时间同步失败,请确保网络连接正常');
}
} catch (error) {
console.error("时间服务初始化失败:", error);
console.error('时间服务初始化失败:', error)
// 降级到本地时间,不影响正常功能
}

this.startTimeUpdate();
await this.startLocationTracking();
await this.loadAttendanceData();
this.startTimeUpdate()
await this.startLocationTracking()
await this.loadAttendanceData()
},

/**
* 开始时间更新(使用稳定的服务器时间更新机制)
*/
startTimeUpdate() {
this.updateCurrentTime();
this.updateCurrentTime()
// 使用timeService的稳定更新回调,不受系统时间影响
this.removeTimeUpdateCallback = timeService.onTimeUpdate(() => {
this.updateCurrentTime();
});
this.updateCurrentTime()
})
},

/**
* 更新当前时间显示(使用服务器时间)
*/
updateCurrentTime() {
this.currentTime = timeService.getCurrentTimeString();
this.currentTime = timeService.getCurrentTimeString()
},

/**
@@ -301,14 +321,14 @@ export default {
const options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0,
};
maximumAge: 0
}

this.watchId = navigator.geolocation.watchPosition(
this.handleLocationSuccess,
this.handleLocationError,
options
);
)
},

/**
@@ -318,9 +338,9 @@ export default {
handleLocationSuccess(position) {
this.userLocation = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
};
this.updateLocationRange();
longitude: position.coords.longitude
}
this.updateLocationRange()
},

/**
@@ -329,13 +349,13 @@ export default {
*/
handleLocationError(error) {
const errorMessages = {
[error.PERMISSION_DENIED]: "请允许获取位置权限",
[error.POSITION_UNAVAILABLE]: "位置信息不可用",
[error.TIMEOUT]: "获取位置超时",
};
[error.PERMISSION_DENIED]: '请允许获取位置权限',
[error.POSITION_UNAVAILABLE]: '位置信息不可用',
[error.TIMEOUT]: '获取位置超时'
}

const message = errorMessages[error.code] || "获取位置失败";
this.$toast.error(message);
const message = errorMessages[error.code] || '获取位置失败'
this.$toast.error(message)
},

/**
@@ -345,22 +365,22 @@ export default {
this.isInRange = AttendanceService.isInAttendanceRange(
this.userLocation,
this.attendanceGroup
);
)
},

/**
* 加载考勤数据
*/
async loadAttendanceData() {
const { userId } = this.userinfo;
const { userId } = this.userinfo
if (!userId) {
throw new Error("用户信息获取失败");
throw new Error('用户信息获取失败')
}

await Promise.all([
this.loadAttendanceGroup(userId),
this.loadTodayAttendance(userId),
]);
this.loadTodayAttendance(userId)
])
},

/**
@@ -368,8 +388,8 @@ export default {
* @param {string} userId 用户ID
*/
async loadAttendanceGroup(userId) {
this.attendanceGroup = await AttendanceService.getAttendanceGroup(userId);
this.updateLocationRange();
this.attendanceGroup = await AttendanceService.getAttendanceGroup(userId)
this.updateLocationRange()
},

/**
@@ -377,9 +397,9 @@ export default {
* @param {string} userId 用户ID
*/
async loadTodayAttendance(userId) {
const records = await AttendanceService.getTodayAttendance(userId);
const records = await AttendanceService.getTodayAttendance(userId)
this.attendanceStatus =
AttendanceService.processAttendanceRecords(records);
AttendanceService.processAttendanceRecords(records)
},

/**
@@ -387,14 +407,14 @@ export default {
*/
async handlePunchClick() {
if (!this.validatePunchConditions()) {
return;
return
}

try {
const punchAction = this.determinePunchAction();
await this.executePunchAction(punchAction);
const punchAction = this.determinePunchAction()
await this.executePunchAction(punchAction)
} catch (error) {
this.handleError("打卡失败", error);
this.handleError('打卡失败', error)
}
},

@@ -405,21 +425,21 @@ export default {
validatePunchConditions() {
if (!this.isInRange) {
// this.$toast.error("请在打卡范围内进行打卡");
this.$toast.error(this.$t("checkin.error.outOfRange"));
return false;
this.$toast.error(this.$t('checkin.error.outOfRange'))
return false
}

const { clockInStatus, clockOutStatus } = this.attendanceStatus;
const { clockInStatus, clockOutStatus } = this.attendanceStatus
if (
clockInStatus !== PUNCH_STATUS.NOT_CHECKED &&
clockOutStatus !== PUNCH_STATUS.NOT_CHECKED
) {
// this.$toast.info("今日已完成打卡");
this.$toast.info(this.$t("checkin.punch.status.todayChecked"));
return false;
this.$toast.info(this.$t('checkin.punch.status.todayChecked'))
return false
}

return true;
return true
},

/**
@@ -427,21 +447,21 @@ export default {
* @returns {Object} 打卡动作信息
*/
determinePunchAction() {
const { clockInStatus } = this.attendanceStatus;
const { clockInStatus } = this.attendanceStatus

if (clockInStatus === PUNCH_STATUS.NOT_CHECKED) {
return AttendanceService.isLateForWork(
this.attendanceGroup.workStartTime
)
? { type: "morning_late", needRemark: true }
: { type: "morning", needRemark: false };
? { type: 'morning_late', needRemark: true }
: { type: 'morning', needRemark: false }
}

return AttendanceService.isEarlyForLeaving(
this.attendanceGroup.workEndTime
)
? { type: "evening_early", needRemark: true }
: { type: "evening", needRemark: false };
? { type: 'evening_early', needRemark: true }
: { type: 'evening', needRemark: false }
},

/**
@@ -450,9 +470,9 @@ export default {
*/
async executePunchAction(action) {
if (action.needRemark) {
this.showRemarkDialogForType(action.type);
this.showRemarkDialogForType(action.type)
} else {
await this.submitPunch(action.type);
await this.submitPunch(action.type)
}
},

@@ -467,14 +487,14 @@ export default {
// };

const titles = {
morning_late: this.$t("checkin.punch.status.lateIn"),
evening_early: this.$t("checkin.punch.status.earlyOut"),
};
this.remarkDialogTitle = titles[type];
this.remarkForm.type = type;
this.remarkForm.remark = "";
this.showRemarkDialog = true;
morning_late: this.$t('checkin.punch.status.lateIn'),
evening_early: this.$t('checkin.punch.status.earlyOut')
}
this.remarkDialogTitle = titles[type]
this.remarkForm.type = type
this.remarkForm.remark = ''
this.showRemarkDialog = true
},

/**
@@ -482,11 +502,11 @@ export default {
*/
async submitRemarkPunch() {
try {
await this.submitPunch(this.remarkForm.type);
this.closeRemarkDialog();
await this.submitPunch(this.remarkForm.type)
this.closeRemarkDialog()
} catch (error) {
// this.handleError("提交失败", error);
this.handleError(this.$t("checkin.error.submitFailed"), error);
this.handleError(this.$t('checkin.error.submitFailed'), error)
}
},

@@ -501,32 +521,32 @@ export default {
userLocation: this.userLocation,
attendanceGroupId: this.attendanceGroup.attendanceGroupId,
attendanceGroupName: this.attendanceGroup.attendanceGroupName,
remark: this.remarkForm.remark,
});
remark: this.remarkForm.remark
})

await AttendanceService.submitPunch(punchData);
await AttendanceService.submitPunch(punchData)

this.currentCompleteDate = AttendanceService.formatDateTime();
this.showSuccessDialog = true;
this.formattedAddress = "Loading...";
this.currentCompleteDate = AttendanceService.formatDateTime()
this.showSuccessDialog = true
this.formattedAddress = 'Loading...'

try {
const res = await getGeocode(
this.userLocation.longitude,
this.userLocation.latitude
);
console.log(res);
const { status, results } = res;
)
console.log(res)
const { status, results } = res
if (status.code === 200 && results.length > 0) {
const { formatted } = results[0];
this.formattedAddress = formatted;
const { formatted } = results[0]
this.formattedAddress = formatted
}
} catch (error) {
this.formattedAddress = this.attendanceGroup.areaName;
console.error(error);
this.formattedAddress = this.attendanceGroup.areaName
console.error(error)
}

await this.loadTodayAttendance(this.userinfo.userId);
await this.loadTodayAttendance(this.userinfo.userId)
},

/**
@@ -538,9 +558,9 @@ export default {
const status =
type === PUNCH_TYPE.CLOCK_IN
? this.attendanceStatus.clockInStatus
: this.attendanceStatus.clockOutStatus;
: this.attendanceStatus.clockOutStatus

return AttendanceService.getStatusText(status);
return AttendanceService.getStatusText(status)
},

/**
@@ -549,22 +569,22 @@ export default {
* @returns {string} 格式化后的时间
*/
formatTime(time) {
return AttendanceService.formatTime(time);
return AttendanceService.formatTime(time)
},

/**
* 关闭备注对话框
*/
closeRemarkDialog() {
this.showRemarkDialog = false;
this.remarkForm.remark = "";
this.showRemarkDialog = false
this.remarkForm.remark = ''
},

/**
* 关闭成功对话框
*/
closeSuccessDialog() {
this.showSuccessDialog = false;
this.showSuccessDialog = false
},

/**
@@ -573,8 +593,8 @@ export default {
* @param {Error} error 错误对象
*/
handleError(message, error) {
console.error(message, error);
this.$toast.error(error.message || message);
console.error(message, error)
this.$toast.error(error.message || message)
},

/**
@@ -582,18 +602,18 @@ export default {
*/
cleanup() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
navigator.geolocation.clearWatch(this.watchId)
}
if (this.removeTimeUpdateCallback) {
this.removeTimeUpdateCallback();
this.removeTimeUpdateCallback()
}
},
},
};
}
}
}
</script>
<style lang="scss" scoped>
.page {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
color: #333;
@media (min-width: 480px) {

Загрузка…
Отмена
Сохранить