Browse Source

✨ feat: 增加考勤及考勤管理页面

Pchen. 8 months ago
parent
commit
4bbf496f9b

+ 68 - 0
src/pages/Admin/ClockIn/ClockInManage.vue

@@ -0,0 +1,68 @@
+<template>
+    <Header />
+    <div class="card">
+        <el-tabs v-model="activeName" class="tabs">
+            <el-tab-pane v-for="(item, index) in tabs" :label="item" :name="item" />
+        </el-tabs>
+
+        <List v-if="activeName === '项目管理'" @edit="Edit" @supplement="supplement"/>
+        <AddItem v-else-if="activeName === '添加项目'" />
+        <EditItem :id="editId" v-else-if="activeName === '编辑项目'" />
+        <Supplement :id="SupplementId" v-else-if="activeName === '考勤补卡'" />
+    </div>
+    <Footer />
+</template>
+
+<script setup>
+import Header from '../../../components/Header.vue';
+import Footer from '../../../components/Footer.vue';
+const List = defineAsyncComponent(() => import('./components/List.vue'));
+const AddItem = defineAsyncComponent(() => import('./components/AddItem.vue'));
+const EditItem = defineAsyncComponent(() => import('./components/EditItem.vue'));
+const Supplement = defineAsyncComponent(() => import('./components/Supplement.vue'));
+document.documentElement.scrollTop = 0;
+const activeName = ref('项目管理');
+let tabs = ref([
+    '项目管理','添加项目'
+])
+let editId = ref();
+let SupplementId = ref();
+
+const Edit = (id) => {
+    if (!tabs.value.includes('编辑项目')) {
+        tabs.value.push(`编辑项目`);
+    }
+    editId.value = id;
+    activeName.value = '编辑项目';
+}
+
+const supplement = (id) => {
+    if (!tabs.value.includes('考勤补卡')) {
+        tabs.value.push(`考勤补卡`);
+    }
+    SupplementId.value = id;
+    activeName.value = '考勤补卡'
+}
+
+</script>
+
+<style scoped>
+.card {
+    width: 80%;
+    min-height: 70vh;
+    background-color: #fff;
+    margin: 0 auto;
+    padding: 35px;
+    border-radius: 10px;
+}
+
+@media only screen and (max-width: 768px) {
+    .card {
+        width: 95%;
+        min-height: 70vh;
+        background-color: transparent;
+        padding: 0px;
+        border-radius: 0px;
+    }
+}
+</style>

+ 241 - 0
src/pages/Admin/ClockIn/components/AddItem.vue

@@ -0,0 +1,241 @@
+<template>
+    <div class="card">
+        <el-form ref="formRef" :model="form" :rules="formRule" label-width="auto" :label-position="'right'" class="form"
+            :size="formsize">
+            <el-form-item label="项目名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入项目名称" />
+            </el-form-item>
+
+            <el-form-item label="参与人" prop="user">
+                <el-input v-model="form.user" placeholder="请输入参与人姓名,用 | 分隔" />
+            </el-form-item>
+
+            <el-form-item label="管理员" prop="admin">
+                <el-input v-model="form.admin" placeholder="请输入管理员姓名,用 | 分隔" />
+            </el-form-item>
+
+            <el-form-item label="打卡星期" prop="day_of_week">
+                <el-select v-model="form.day_of_week" placeholder="请选择星期">
+                    <el-option label="星期一" value="1" />
+                    <el-option label="星期二" value="2" />
+                    <el-option label="星期三" value="3" />
+                    <el-option label="星期四" value="4" />
+                    <el-option label="星期五" value="5" />
+                    <el-option label="星期六" value="6" />
+                    <el-option label="星期日" value="0" />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item label="每周循环" prop="loopy">
+                <el-radio-group v-model="form.loopy">
+                    <el-radio label="是" />
+                    <el-radio label="否" />
+                </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="打卡时间" prop="time">
+                <el-config-provider :locale="zhCn">
+                    <el-time-picker v-model="form.time" is-range range-separator="~" start-placeholder="开始时间"
+                        end-placeholder="结束时间" value-format="HH:mm:ss" />
+                </el-config-provider>
+            </el-form-item>
+
+            <el-form-item label="打卡半径" prop="radius">
+                <el-input v-model="form.radius" type="number" placeholder="输入打卡范围半径 单位米" />
+            </el-form-item>
+
+            <el-form-item label="打卡地点" prop="address">
+                <el-input v-model="form.address" placeholder="请在地图中选择地点" disabled />
+            </el-form-item>
+
+            <el-form-item label="经纬度" prop="position">
+                <el-input v-model="form.position" placeholder="请在地图中选择地点" disabled />
+            </el-form-item>
+
+        </el-form>
+
+        <MapContainer :position="form.position" :radius="form.radius" @update="update" />
+
+        <el-button class="button" round @click="onSubmit(formRef)">提交</el-button>
+    </div>
+</template>
+
+<script setup>
+import { reactive, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { ServerAPI } from '../../../../app/lib/ServerAPI';
+import { App } from '../../../../app/app';
+import MapContainer from './MapContainer.vue';
+import zhCn from 'element-plus/es/locale/lang/zh-cn';
+
+let formsize = ref("large");
+let router = useRouter();
+
+if (window.innerWidth <= 768) {
+    formsize.value = ''
+}
+
+if (!App.hasUser()) {
+    ElMessage({
+        message: "请登录",
+        type: 'warning',
+    });
+    router.replace({
+        name: "Login"
+    });
+}
+
+let formRef = ref();
+
+let form = reactive({
+    name: '',
+    user: '',
+    admin: '',
+    day_of_week: '',
+    loopy: false,
+    time: '',
+    loopy: '否',
+    address: '重庆市南岸区海棠溪街道重庆工商大学南岸校区大学生创业孵化园',
+    position: [106.5799475868821, 29.504864472181577],
+    radius: 50
+});
+
+let formRule = reactive({
+    name: [
+        { required: true, message: '此项为必填项' },
+        { min: 2, max: 18, message: '长度应为2-18位' }
+    ],
+    user: [
+        { required: true, message: '此项为必填项' }
+    ],
+    day_of_week: [
+        { required: true, message: '此项为必填项' }
+    ],
+    loopy: [
+        { required: true, message: '此项为必填项' }
+    ],
+    time: [
+        { required: true, message: '此项为必填项' }
+    ],
+    radius: [
+        { required: true, message: '此项为必填项' }
+    ],
+})
+
+let update = (data, address) => {
+    form.position = data;
+    form.address = address;
+}
+
+let onSubmit = async function (formEl) {
+    if (!App.hasUser()) {
+        ElMessage({
+            message: "请登录",
+            type: 'warning',
+        });
+        router.replace({
+            name: "Login",
+        });
+        return;
+    }
+
+    if (!formEl) return;
+
+    await formEl.validate((valid, fields) => {
+
+        if (!valid) {
+            ElMessage({
+                message: "请检查填写的内容",
+                type: "warning"
+            });
+            return;
+        }
+
+        const loading = ElLoading.service({
+            lock: true,
+            text: '正在提交中,请稍候'
+        })
+
+        let data = {
+            uuid: App.user.uuid,
+            session: App.user.session,
+            name: form.name,
+            user: form.user,
+            admin: form.admin,
+            day_of_week: form.day_of_week,
+            loopy: form.loopy == "是" ? 1 : 0,
+            begintime: form.time[0],
+            endtime: form.time[1],
+            address: form.address,
+            position: form.position,
+            radius: form.radius
+        };
+
+        ServerAPI.AddAttendanceItems(data, (r) => {
+            if (r == undefined) {
+                loading.close();
+                ElMessage({
+                    message: "服务器未响应",
+                    type: "error"
+                });
+                return;
+            }
+
+            if (r.code != 0) {
+                loading.close();
+                ElMessage({
+                    message: `错误:${r.msg}(${r.endpoint})`,
+                    type: "error"
+                });
+                return;
+            }
+
+            ElMessage({
+                message: "已提交",
+                type: "success"
+            });
+            loading.close();
+            router.go(0);
+        })
+
+    })
+
+}
+
+</script>
+
+<style scoped>
+.card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.form {
+    width: 60%;
+    margin: 0 auto;
+    color: black;
+}
+
+.button {
+    background-color: #c8161d;
+    color: white;
+    width: 200px;
+    border: none;
+    margin-top: 20px;
+}
+
+@media only screen and (max-width: 768px) {
+    .card {
+        background-color: rgba(255, 255, 255, 0.6);
+        width: 80%;
+        border-radius: 10px;
+        padding: 20px;
+    }
+
+    .form {
+        width: 100%;
+    }
+
+}
+</style>

+ 253 - 0
src/pages/Admin/ClockIn/components/EditItem.vue

@@ -0,0 +1,253 @@
+<template>
+    <div class="card">
+        <el-form ref="formRef" :model="form" :rules="formRule" label-width="auto" :label-position="'right'" class="form"
+            :size="formsize" v-if="show">
+            <el-form-item label="项目名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入项目名称" />
+            </el-form-item>
+
+            <el-form-item label="参与人" prop="user">
+                <el-input v-model="form.user" placeholder="请输入参与人姓名,用 | 分隔" />
+            </el-form-item>
+
+            <el-form-item label="管理员" prop="admin" v-if="createUser === App.user.uuid">
+                <el-input v-model="form.admin" placeholder="请输入管理员姓名,用 | 分隔" />
+            </el-form-item>
+
+            <el-form-item label="打卡星期" prop="day_of_week">
+                <el-select v-model="form.day_of_week" placeholder="请选择星期">
+                    <el-option label="星期一" value="1" />
+                    <el-option label="星期二" value="2" />
+                    <el-option label="星期三" value="3" />
+                    <el-option label="星期四" value="4" />
+                    <el-option label="星期五" value="5" />
+                    <el-option label="星期六" value="6" />
+                    <el-option label="星期日" value="0" />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item label="每周循环" prop="loopy">
+                <el-radio-group v-model="form.loopy">
+                    <el-radio label="是" />
+                    <el-radio label="否" />
+                </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="打卡时间" prop="time">
+                <el-config-provider :locale="zhCn">
+                    <el-time-picker v-model="form.time" is-range range-separator="~" :start-placeholder="form.time[0]"
+                        :end-placeholder="form.time[1]" value-format="HH:mm:ss" />
+                </el-config-provider>
+            </el-form-item>
+
+            <el-form-item label="打卡半径" prop="radius">
+                <el-input v-model="form.radius" type="number" placeholder="输入打卡范围半径 单位米" />
+            </el-form-item>
+
+            <el-form-item label="打卡地点" prop="address">
+                <el-input v-model="form.address" placeholder="请在地图中选择地点" disabled />
+            </el-form-item>
+
+            <el-form-item label="经纬度" prop="endtime">
+                <el-input v-model="form.position" placeholder="请在地图中选择地点" disabled />
+            </el-form-item>
+
+        </el-form>
+
+        <MapContainer :position="form.position" :radius="form.radius" @update="update" />
+
+        <el-button class="button" round @click="onSubmit(formRef)">提交</el-button>
+    </div>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { ServerAPI } from '../../../../app/lib/ServerAPI';
+import { App } from '../../../../app/app';
+import MapContainer from './MapContainer.vue';
+import zhCn from 'element-plus/es/locale/lang/zh-cn';
+
+let formsize = ref("large");
+let router = useRouter();
+let props = defineProps({
+    id: {
+        type: Number
+    }
+});
+
+if (window.innerWidth <= 768) {
+    formsize.value = ''
+}
+
+if (!App.hasUser()) {
+    ElMessage({
+        message: "请登录",
+        type: 'warning',
+    });
+    //前往登录
+    router.replace({
+        name: "Login"
+    });
+}
+
+let formRef = ref();
+let show = ref(false);
+let createUser = ref('');
+
+let form = reactive({
+    name: '',
+    user: '',
+    admin: '',
+    day_of_week: '',
+    loopy: false,
+    time: '',
+    loopy: '否',
+    address: '',
+    position: [106.5799475868821, 29.504864472181577],
+    radius: 50
+});
+
+onMounted(() => {
+    ServerAPI.GetAttendanceItemDetail(App.user.uuid, App.user.session, props.id, (r) => {
+        if(r && r.code === 0) {
+            Object.assign(form, r.data);
+            form.time = [r.data.begintime, r.data.endtime];
+            r.data.loopy === 1 ? form.loopy = '是' : form.loopy = '否';
+            form.user = r.data.user.map(uuid => r.userInfo[uuid].username).join('|');
+            if(form.admin) {
+                form.admin = r.data.admin.map(uuid => r.userInfo[uuid].username).join('|');       
+            }
+            createUser.value = r.data.createUser;
+            show.value = true
+        } else {
+            ElMessage.error('获取项目详情失败!');
+        }
+    })
+})
+
+let formRule = reactive({
+    name: [
+        { required: true, message: '此项为必填项' },
+        { min: 2, max: 18, message: '长度应为2-18位' }
+    ],
+    user: [
+        { required: true, message: '此项为必填项' }
+    ],
+    day_of_week: [
+        { required: true, message: '此项为必填项' }
+    ],
+    loopy: [
+        { required: true, message: '此项为必填项' }
+    ],
+    time: [
+        { required: true, message: '此项为必填项' }
+    ],
+    radius: [
+        { required: true, message: '此项为必填项' }
+    ],
+})
+
+let update = (data, address) => {
+    form.position = data;
+    form.address = address;
+}
+
+let onSubmit = async function (formEl) {
+    if (!App.hasUser()) {
+        ElMessage({
+            message: "请登录",
+            type: 'warning',
+        });
+        router.replace({
+            name: "Login",
+        });
+        return;
+    }
+
+    if (!formEl) return;
+
+    await formEl.validate((valid, fields) => {
+
+        if (!valid) {
+            ElMessage({
+                message: "请检查填写的内容",
+                type: "warning"
+            });
+            return;
+        }
+
+        let data = {
+            uuid: App.user.uuid,
+            session: App.user.session,
+            id: props.id,
+            name: form.name,
+            admin: form.admin,
+            user: form.user,
+            day_of_week: form.day_of_week,
+            loopy: form.loopy == "是" ? 1 : 0,
+            begintime: form.time[0],
+            endtime: form.time[1],
+            address: form.address,
+            position: form.position,
+            radius: form.radius
+        };
+
+        ServerAPI.EditAttendanceItems(data, (r) => {
+            if (r == undefined) {
+                ElMessage.error('服务器未响应');
+                return;
+            }
+
+            if (r.code != 0) {
+                ElMessage({
+                    message: `修改失败!${r.msg}`,
+                    type: "error"
+                });
+                return;
+            }
+
+            ElMessage.success('已成功修改!');
+            router.go(0);
+        })
+
+    })
+
+}
+
+</script>
+
+<style scoped>
+.card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.form {
+    width: 60%;
+    margin: 0 auto;
+    color: black;
+}
+
+.button {
+    background-color: #c8161d;
+    color: white;
+    width: 200px;
+    border: none;
+    margin-top: 20px;
+}
+
+@media only screen and (max-width: 768px) {
+    .card {
+        background-color: rgba(255, 255, 255, 0.6);
+        width: 80%;
+        border-radius: 10px;
+        padding: 20px;
+    }
+
+    .form {
+        width: 100%;
+    }
+
+}
+</style>

+ 139 - 0
src/pages/Admin/ClockIn/components/List.vue

@@ -0,0 +1,139 @@
+<template>
+    <el-table :data="data" style="width: 100%" highlight-current-row>
+        <el-table-column fixed prop="id" label="ID" width="45" />
+        <el-table-column prop="name" label="项目名称" />
+        <el-table-column label="创建人" width="80">
+            <template #default="scope">
+                {{ userInfo[scope.row.createUser].username }}
+            </template>
+        </el-table-column>
+        <el-table-column label="创建时间">
+            <template #default="scope">
+                {{ new Date(Number(scope.row.createTime)).toLocaleDateString() }}
+            </template>
+        </el-table-column>
+        <el-table-column label="星期" width="68">
+            <template #default="scope">
+                {{ getWeekday(scope.row.day_of_week) }}
+            </template>
+        </el-table-column>
+        <el-table-column label="重复" width="53">
+            <template #default="scope">
+                {{ scope.row.loopy ? '是' : '否' }}
+            </template>
+        </el-table-column>
+        <el-table-column label="打卡时间">
+            <template #default="scope">
+                {{ scope.row.begintime }}~{{ scope.row.endtime }}
+            </template>
+        </el-table-column>
+        <el-table-column label="管理员">
+            <template #default="scope">
+                <div style="margin: 5px" v-for="(item, index) in scope.row.admin" :key="index">
+                    <el-tag v-if="item !== 'default'">
+                        {{ userInfo[item].username }}
+                    </el-tag>
+                </div>
+            </template>
+        </el-table-column>
+        <el-table-column label="参与人">
+            <template #default="scope">
+                <div style="margin: 5px" v-for="(item, index) in scope.row.user" :key="index">
+                    <el-tag v-if="item !== 'default'">
+                        {{ userInfo[item].username }}
+                    </el-tag>
+                </div>
+            </template>
+        </el-table-column>
+        <el-table-column prop="address" label="打卡位置" />
+        <el-table-column prop="radius" label="半径" width="60" />
+        <el-table-column fixed="right" label="操作">
+            <template #default="scope">
+
+                <el-button link type="primary" size="small" style="margin-left: 12px;"
+                    @click="$router.push(`/ClockIn/${scope.row.id}`)">查看</el-button>
+                <el-button link type="primary" size="small" @click="Edit(scope.row.id)">编辑</el-button>
+                <el-button link type="primary" size="small" @click="Supplement(scope.row.id)">补卡</el-button>
+                <el-button link type="primary" size="small" @click="Delete(scope.row.id)">
+                    删除
+                </el-button>
+
+            </template>
+        </el-table-column>
+    </el-table>
+</template>
+
+<script setup>
+import { App } from '../../../../app/app.js';
+import { ServerAPI } from '../../../../app/lib/ServerAPI';
+
+let data = ref();
+let userInfo = ref();
+const emit = defineEmits(['edit', 'supplement']);
+
+const onload = () => {
+    ServerAPI.GetAttendanceItemList(App.user.uuid, App.user.session, (r) => {
+        if (r && r.code === 0) {
+            data.value = r.data;
+            userInfo.value = r.userInfo;
+        } else {
+            ElMessage.error(`拉取考勤列表失败!${r.msg}`);
+        }
+    })
+}
+onload()
+
+const Edit = (id) => {
+    emit('edit', id);
+}
+
+const Supplement = (id) => {
+    emit('supplement', id);
+}
+
+const Delete = (id) => {
+    ElMessageBox.confirm(
+        '是否删除该考勤项目?此操作会同时删除已有的考勤记录',
+        '警告',
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    )
+        .then(() => {
+            ServerAPI.DeleteAttendanceItem(App.user.uuid, App.user.session, id, (r) => {
+                if (r && r.code === 0) {
+                    data.value = r.data;
+                    userInfo.value = r.userInfo;
+                    ElMessage.success('删除成功!');
+                    onload();
+                } else {
+                    ElMessage.error("删除失败!" + (r.msg ? r.msg : ''));
+                }
+            })
+        })
+}
+
+function getWeekday(day_of_week) {
+    switch (day_of_week) {
+        case 0:
+            return '星期日'
+        case 1:
+            return '星期一'
+        case 2:
+            return '星期二'
+        case 3:
+            return '星期三'
+        case 4:
+            return '星期四'
+        case 5:
+            return '星期五'
+        case 6:
+            return '星期六'
+
+        default:
+            return '未知星期'
+    }
+}
+</script>

+ 151 - 0
src/pages/Admin/ClockIn/components/MapContainer.vue

@@ -0,0 +1,151 @@
+<template>
+  <div id="container"></div>
+</template>
+
+<script setup>
+import { defineEmits } from 'vue';
+import AMapLoader from "@amap/amap-jsapi-loader";
+
+const emit = defineEmits(['update']);
+
+let props = defineProps({
+  position: {
+    type: Object,
+    default: () => ([])
+  },
+  radius: {
+    type: Number,
+    default: () => (50)
+  }
+});
+
+watch(
+  () => [props.position, props.radius],
+  () => {
+    removeCircle()
+    drawCircle()
+  }
+);
+
+let map = null;
+let marker = ref('');
+let circle = ref('');
+let address = '';
+let geocoder;
+
+window._AMapSecurityConfig = {
+  serviceHost: "https://nsrh.ctbu.edu.cn/_AMapService",
+};
+
+onMounted(() => {
+  AMapLoader.load({
+    key: "d1f123693def8a412c976184daa4b60e",
+    version: "2.0",
+  })
+    .then((AMap) => {
+      map = new AMap.Map("container", {
+        viewMode: '2D',
+        zoom: 17,
+        center: props.position,
+      });
+
+      AMap.plugin(["AMap.ToolBar", "AMap.Scale", "AMap.CircleEditor", "AMap.Geocoder"], function () {
+        let toolbar = new AMap.ToolBar();
+        map.addControl(toolbar);
+        let scale = new AMap.Scale();
+        map.addControl(scale);
+
+        drawCircle()
+
+        geocoder = new AMap.Geocoder({
+          city: "全国",
+        });
+      });
+
+      marker.value = new AMap.Marker({
+        position: props.position,
+        title: "考勤地点",
+      });
+      map.add(marker.value);
+
+      map.on('click', (e) => {
+        removeMarker();
+        marker.value = new AMap.Marker({
+          position: e.lnglat,
+          title: "考勤地点",
+        });
+        map.add(marker.value);
+        getAddress(e.lnglat);
+      });
+    })
+    .catch((e) => {
+      console.log(e);
+    });
+
+});
+
+//获取地址信息
+const getAddress = async (position) => {
+  const loading = ElLoading.service({
+    lock: true,
+    text: '正在加载中,请稍候'
+  })
+  await geocoder.getAddress(position, function (status, result) {
+    if (status === "complete" && result.info === "OK") {
+      address = result.regeocode.formattedAddress;
+    } else {
+      address = '未知';
+    }
+    loading.close();
+    //触发更新事件
+    emit('update', [position.KL, position.kT], address);
+  })
+}
+
+const drawCircle = () => {
+  //绘制签到范围
+  circle.value = new AMap.Circle({
+    center: props.position,
+    radius: props.radius,
+    borderWeight: 1,
+    strokeOpacity: 1,
+    strokeOpacity: 0.2,
+    fillOpacity: 0.4,
+  })
+
+  circle.value.setMap(map)
+  map.setFitView([circle.value])
+  new AMap.CircleEditor(map, circle.value)
+}
+
+const removeCircle = () => {
+  if (circle.value) {
+    map.remove(circle.value);
+    circle.value = ''
+  }
+}
+
+const removeMarker = () => {
+  if (marker.value) {
+    map.remove(marker.value);
+    marker.value = ''
+  }
+}
+
+onUnmounted(() => {
+  map?.destroy();
+});
+</script>
+
+<style scoped>
+#container {
+  width: 100%;
+  height: 450px;
+}
+
+@media only screen and (max-width: 768px) {
+  #container {
+    height: 350px;
+  }
+}
+</style>

+ 137 - 0
src/pages/Admin/ClockIn/components/Supplement.vue

@@ -0,0 +1,137 @@
+<template>
+    <div class="card">
+        <el-form ref="formRef" :model="form" :rules="formRule" label-width="auto" :label-position="'right'" class="form"
+            :size="formsize">
+            <el-form-item label="项目编号" prop="id">
+                <el-input v-model="form.id" disabled />
+            </el-form-item>
+            <el-form-item label="补卡人" prop="user">
+                <el-input v-model="form.user" placeholder="请输入补卡人姓名" />
+            </el-form-item>
+        </el-form>
+
+        <el-button class="button" round @click="onSubmit(formRef)">提交</el-button>
+    </div>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router';
+import { ServerAPI } from '../../../../app/lib/ServerAPI';
+import { App } from '../../../../app/app';
+
+let formsize = ref("large");
+let router = useRouter();
+let props = defineProps({
+    id: {
+        type: Number
+    }
+});
+
+if (window.innerWidth <= 768) {
+    formsize.value = ''
+}
+
+let formRef = ref();
+
+let form = reactive({
+    id: props.id,
+    user: '',
+});
+
+let formRule = reactive({
+    user: [
+        { required: true, message: '此项为必填项' }
+    ],
+})
+
+let onSubmit = async function (formEl) {
+    if (!App.hasUser()) {
+        ElMessage({
+            message: "请登录",
+            type: 'warning',
+        });
+        router.replace({
+            name: "Login",
+        });
+        return;
+    }
+
+    if (!formEl) return;
+
+    await formEl.validate((valid, fields) => {
+
+        if (!valid) {
+            ElMessage({
+                message: "请检查填写的内容",
+                type: "warning"
+            });
+            return;
+        }
+
+        const loading = ElLoading.service({
+            lock: true,
+            text: '正在加载中,请稍候'
+        })
+
+        ServerAPI.SupplementRecord(App.user.uuid, App.user.session, form.user, props.id, (r) => {
+            if (r == undefined) {
+                loading.close();
+                ElMessage.error('服务器未响应');
+                return;
+            }
+
+            if (r.code != 0) {
+                loading.close();
+                ElMessage({
+                    message: `错误:${r.msg}`,
+                    type: "error"
+                });
+                return;
+            }
+
+            loading.close();
+            ElMessage.success('补卡成功!');
+            form.user = '';
+        })
+
+    })
+
+}
+
+</script>
+
+<style scoped>
+.card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.form {
+    width: 60%;
+    margin: 0 auto;
+    color: black;
+}
+
+.button {
+    background-color: #c8161d;
+    color: white;
+    width: 200px;
+    border: none;
+    margin-top: 20px;
+}
+
+@media only screen and (max-width: 768px) {
+    .card {
+        background-color: rgba(255, 255, 255, 0.6);
+        width: 80%;
+        border-radius: 10px;
+        padding: 20px;
+    }
+
+    .form {
+        width: 100%;
+    }
+
+}
+</style>

+ 483 - 0
src/pages/ClockIn/ClockIn.vue

@@ -0,0 +1,483 @@
+<template>
+    <div class="header">
+        <Header :color="'black'"/>
+    </div>
+
+    <div class="root">
+        <MapContainer @update="Update" @err="Err" v-if="status === 1 && data != null" :radius="data.radius"
+            :position="data.position" :address="data.address" />
+        <div class="container" v-else></div>
+
+        <div class="content">
+            <div class="item">
+                <el-result icon="success" title="您在打卡范围内" sub-title="现在可进行打卡操作"
+                    v-if="status === 1 && distance <= data.radius">
+                    <template #extra>
+                        <el-button type="success" @click="addRecords">立即打卡</el-button>
+                    </template>
+                </el-result>
+
+                <el-result icon="warning" title="您不在打卡范围内" :sub-title="'距打卡点' + distance + '米'"
+                    v-else-if="status === 1 && distance > data.radius">
+                    <template #extra>
+                        <el-button type="warning" @click="$router.go(0)">重新定位</el-button>
+                    </template>
+                </el-result>
+
+                <el-result icon="success" title="打卡成功" :sub-title="subTitle" v-else-if="status === 2">
+                    <template #extra>
+                        <el-button type="success" @click="$router.push('/')">返回首页</el-button>
+                    </template>
+                </el-result>
+
+                <div class="err" v-else>
+                    <el-result icon="error" :title="Title ? Title : '当前无法进行打卡操作'" :sub-title="subTitle">
+                        <template #extra>
+                            <el-button type="danger" @click="onload">重新尝试</el-button>
+                        </template>
+                    </el-result>
+                </div>
+            </div>
+
+            <div class="item" v-if="data">
+                <p class="title">项目详情</p>
+
+                <div class="info">
+                    <div class="key">项目编号:</div>
+                    <div class="value">{{ data.id }}</div>
+                </div>
+
+                <div class="info">
+                    <div class="key">项目名称:</div>
+                    <div class="value">{{ data.name }}</div>
+                </div>
+
+                <div class="info">
+                    <div class="key">创建时间:</div>
+                    <div class="value">{{ stramptoTime(data.createTime) }}</div>
+                </div>
+
+                <div class="info">
+                    <div class="key">打卡星期:</div>
+                    <div class="value">{{ getWeekday(data.day_of_week) }}</div>
+                </div>
+
+                <div class="info">
+                    <div class="key">每周循环:</div>
+                    <div class="value">{{ data.loopy ? '是' : '否' }}</div>
+                </div>
+
+                <div class="info">
+                    <div class="key">打卡地点:</div>
+                    <div class="value">{{ data.address }}</div>
+                </div>
+
+                <div class="info">
+                    <div class="key">打卡时间:</div>
+                    <div class="value">
+                        {{ data.begintime }}~{{ data.endtime }}
+                    </div>
+                </div>
+
+                <div class="info users">
+                    <div class="key">发起人:</div>
+                    <div class="value2">
+                        <div class="useritem">
+                            <el-avatar :size="23" :src="userInfo[data.createUser].avatar" />
+                            {{ userInfo[data.createUser] !=
+                            undefined ? userInfo[data.createUser].username : `用户${data.createUser.slice(0, 4)}` }}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="info users">
+                    <div class="key">管理员:</div>
+                    <div class="value2">
+                        <div v-if="data.admin && data.admin.length === 0">无</div>
+                        <div class="useritem" v-for="(item, index) in data.admin">
+                            <el-avatar :size="23" :src="userInfo[item].avatar" />
+                            {{ userInfo[item] !=
+                            undefined ? userInfo[item].username : `用户${item.slice(0, 4)}` }}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="info users">
+                    <div class="key">参与人:</div>
+                    <div class="value2">
+                        <div class="useritem join" v-for="(item, index) in data.user">
+                            <el-avatar :size="23" :src="userInfo[item].avatar" />
+                            {{ userInfo[item] !=
+                            undefined ? userInfo[item].username : `用户${item.slice(0, 4)}` }}
+                        </div>
+                    </div>
+                </div>
+                <div class="info users">
+                    <div class="key">未打卡:</div>
+                    <div class="value2">
+                        <div v-if="noRecord.length === 0">无</div>
+                        <div class="useritem fail" v-for="(item, index) in noRecord">
+                            <el-avatar :size="23" :src="userInfo[item].avatar" />
+                            {{ userInfo[item] !=
+                            undefined ? userInfo[item].username : `用户${item.slice(0, 4)}` }}
+                        </div>
+                    </div>
+                </div>
+                <div class="info users">
+                    <div class="key">已打卡:</div>
+                    <div class="value2">
+                        <div v-if="thisWeekRecords.length ===0">无
+                        </div>
+                        <div class="useritem success" v-for="(item, index) in thisWeekRecords">
+                            <el-avatar :size="23" :src="userInfo[item.uuid].avatar" />
+                            {{ userInfo[item.uuid] !=
+                            undefined ? userInfo[item.uuid].username : `用户${item.uuid.slice(0, 4)}` }}
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="item" v-if="data">
+                <p class="title">打卡记录</p>
+                <el-timeline>
+                    <el-timeline-item type="success" :timestamp="stramptoTime(data.createTime)">
+                        打卡项目已创建
+                    </el-timeline-item>
+                    <el-timeline-item type="danger" v-if="(showAll ? records : thisWeekRecords).length == 0">
+                        暂时没有打卡记录
+                    </el-timeline-item>
+                    <el-timeline-item type="success" v-for="(item, index) in showAll ? records : thisWeekRecords"
+                        :timestamp="stramptoTime(item.time)">
+                        {{ userInfo[item.uuid] !==
+                        undefined ? userInfo[item.uuid].username : `用户${item.uuid.slice(0, 4)}` }}已打卡{{ item.commit ?
+                        ('(' + item.commit + ')')
+                        : '' }}
+                    </el-timeline-item>
+                </el-timeline>
+
+                <div v-if="data.loopy === 1">
+                    <div class="hint" v-if="!showAll">当前显示本周期记录 | <span @click="showAll = true">查看全部</span></div>
+                    <div class="hint" v-else>当前显示全部记录 | <span @click="showAll = false">查看当前周期</span></div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <el-backtop :right="70" :bottom="70"/>
+    <Footer />
+</template>
+
+<script setup>
+import Header from '../../components/Header.vue';
+import Footer from '../../components/Footer.vue';
+import { App } from '../../app/app';
+import { ServerAPI } from '../../app/lib/ServerAPI';
+const MapContainer = defineAsyncComponent(() => import('./components/MapContainer.vue'));
+import { useRoute, useRouter } from 'vue-router';
+
+const route = useRoute();
+const router = useRouter();
+
+let status = ref(0);
+let Title = ref('');
+let subTitle = ref('');
+let distance = ref();
+
+let data = ref();
+let userInfo = ref({});
+let records = ref([]);
+let thisWeekRecords = ref([]);
+let showAll = ref(false);
+let noRecord = ref([]);
+
+function Update(data) {
+    distance.value = data;
+}
+
+function Err(data) {
+    status.value = 0;
+    Title.value = '定位时发生错误';
+    subTitle.value = data;
+}
+
+const stramptoTime = function (time) {
+    if (time < 10)
+        return 'NaN';
+    return new Date(+time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
+}
+
+function onload() {
+    document.documentElement.scrollTop = 0;
+    const id = route.params.id;
+    if (!App.hasUser()) {
+        ElMessage.error('登录已过期,请重新登录');
+        return router.push('/login');
+    }
+
+    const loading = ElLoading.service({
+        lock: true,
+        text: '正在加载中,请稍候'
+    })
+
+    ServerAPI.GetAttendanceItemDetail(App.user.uuid, App.user.session, id, (r) => {
+        if (!r || r.code != 0 || r.data.length === 0) {
+            Title.value = '获取考勤数据失败,请稍后再试';
+            loading.close();
+            return ElMessage.error('获取考勤数据失败!' + (r.msg ? r.msg : ''))
+        }
+        data.value = r.data;
+        userInfo.value = r.userInfo;
+        records.value = r.records;
+        thisWeekRecords.value = filterRecords();
+        noRecord.value = r.data.user.filter(uuid =>
+            !thisWeekRecords.value.some(record => record.uuid === uuid)
+        );
+        loading.close();
+
+        if (!r.data.user.includes(App.user.uuid)) {
+            return Title.value = '您不在考勤名单内';
+        }
+
+        if (records.value.length !== 0 && hasRecord(data.value, records.value)) {
+            subTitle.value = `打卡时间:${stramptoTime(Number(thisWeekRecords.value.find(record => record.uuid === App.user.uuid).time))}`;
+            return status.value = 2;
+        }
+
+        const isWithinTime = isWithinAttendanceTime(data.value);
+        if (!isWithinTime) {
+            Title.value = '不在考勤时间范围内';
+            return subTitle.value = `考勤时间:${getWeekday(data.value.day_of_week)} ${data.value.begintime}~${data.value.endtime}`;
+        }
+
+        // if (window.innerWidth > 768 || ) {
+        //     return Title.value = '请使用手机端企业微信进行打卡操作';
+        // }
+
+        status.value = 1
+    })
+}
+onload();
+
+function isWithinAttendanceTime(attendanceTimes) {
+    const now = new Date();
+    const dayOfWeek = now.getDay();
+    const nowTime = now.getTime();
+
+    function timeToTodayTimestamp(timeStr) {
+        const [hours, minutes, seconds] = timeStr.split(':').map(Number);
+        const todayWithTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, seconds);
+        return todayWithTime.getTime();
+    }
+
+    const { createTime, day_of_week, begintime, endtime, loopy } = attendanceTimes
+    if (dayOfWeek === day_of_week && (loopy || (Number(createTime) + 604800000 - nowTime > 0)) && timeToTodayTimestamp(begintime) <= nowTime && timeToTodayTimestamp(endtime) >= nowTime) {
+        return true;
+    }
+    return false
+}
+
+function addRecords() {
+    if (distance.value > data.value.radius) {
+        return ElMessage.error('不在打卡范围内!');
+    }
+
+    ServerAPI.AddAttendanceRecord(App.user.uuid, App.user.session, route.params.id, (r) => {
+        if (r && r.code === 0) {
+            ElMessage.success('打卡成功!');
+            onload();
+        } else {
+            ElMessage.error('打卡失败!' + (r.msg ? r.msg : ''));
+        }
+    })
+}
+
+function filterRecords() {
+    const { day_of_week, begintime, loopy } = data.value;
+    if (!loopy) {
+        return records.value;
+    }
+    const now = new Date();
+    const nowTime = now.getTime();
+
+    const begin = getTimestamp(day_of_week, begintime);
+    if (nowTime >= begin) {
+        return records.value.filter(item => item.time >= begin);
+    }
+    return records.value.filter(item => item.time >= begin - 604800000);
+}
+
+function getTimestamp(day_of_week, begintime) {
+    const now = new Date();
+    const dayOfWeek = now.getDay();
+    // 计算今天与目标星期几的差值
+    // 目标星期几应当是从0到6的范围,0是星期日,6是星期六
+    const daysUntilTarget = (day_of_week - dayOfWeek + 7) % 7;
+    // 计算目标日期
+    const targetDate = new Date(now);
+    targetDate.setDate(now.getDate() + daysUntilTarget);
+    const [hours, minutes, seconds] = begintime.split(':').map(Number);
+    targetDate.setHours(hours, minutes, seconds, 0);
+    return targetDate.getTime();
+}
+
+function hasRecord(attendanceData, records) {
+    const { day_of_week, begintime, loopy } = attendanceData;
+    if (!loopy) {
+        return records.some(record => record.uuid === App.user.uuid);
+    }
+    const now = new Date();
+    const nowTime = now.getTime();
+
+    const begin = getTimestamp(day_of_week, begintime);
+    if (nowTime >= begin) {
+        return records.some(record => record.time >= begin && record.uuid === App.user.uuid);
+    }
+
+    return records.some(record => record.time >= begin - 604800000 && record.uuid === App.user.uuid);
+}
+
+function getWeekday(day_of_week) {
+    switch (day_of_week) {
+        case 0:
+            return '星期日'
+        case 1:
+            return '星期一'
+        case 2:
+            return '星期二'
+        case 3:
+            return '星期三'
+        case 4:
+            return '星期四'
+        case 5:
+            return '星期五'
+        case 6:
+            return '星期六'
+
+        default:
+            return '未知星期'
+    }
+}
+
+</script>
+
+<style scoped>
+.header {
+    position: fixed;
+    z-index: 999;
+    width: 100%;
+}
+
+.container {
+    position: sticky;
+    height: 80px;
+}
+
+.root {
+    display: flex;
+    flex-direction: column;
+    min-height: 70vh;
+    gap: 20px;
+}
+
+.content {
+    flex: 1;
+    width: 70%;
+    margin: 0 auto;
+    background-color: #eee;
+}
+
+.content .title {
+    font-size: 1.2em;
+    text-align: center;
+}
+
+.content .item {
+    padding: 8px 20px 20px 20px;
+    background-color: #fff;
+    border-radius: 10px;
+    margin-bottom: 10px;
+    color: #337ecc;
+    animation: fadeIn 0.6s ease-in-out forwards;
+}
+
+.content .item .info {
+    display: flex;
+    font-size: 14px;
+    margin-top: 4px;
+}
+
+.content .item .info .key {
+    width: 70px;
+}
+
+.content .item .info .value {
+    color: #777;
+    flex: 1;
+}
+
+.content .item .value2 {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 5px;
+}
+
+.content .item .useritem {
+    display: flex;
+    align-items: center;
+    background-color: #d9ecff;
+    color: #337ecc;
+    padding: 5px;
+    border-radius: 10px;
+    transform: translateY(-7px);
+}
+
+.content .item .join {
+    background-color: #faecd8;
+    color: #b88230;
+}
+
+.content .item .success {
+    background-color: #e1f3d8;
+    color: #529b2e
+}
+
+.content .item .fail {
+    background-color: #fde2e2;
+    color: #c45656
+}
+
+.content .item .users {
+    margin-top: 10px;
+}
+
+.hint {
+    color: #888;
+    font-size: 0.8em;
+    text-align: center
+}
+
+.err {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.el-timeline {
+    margin-top: 20px;
+}
+
+@media only screen and (max-width: 768px) {
+    .content {
+        width: 90%;
+        margin-top: -10px;
+    }
+
+    .container {
+        height: 60px
+    }
+
+    .el-timeline {
+        margin-left: -35px;
+    }
+}
+</style>

+ 234 - 0
src/pages/ClockIn/ClockInList.vue

@@ -0,0 +1,234 @@
+<template>
+    <Header />
+    <div class="root">
+        <div class="content">
+            <el-result icon="warning" title="暂时没有打卡任务" v-if="data.length === 0">
+                <template #extra>
+                    <el-button type="warning" @click="onload">重新加载</el-button>
+                </template>
+            </el-result>
+            <div v-for="(item, index) in data" :key="item.id" class="item" @click="$router.push(`/ClockIn/${item.id}`)">
+                <div class="info2">
+                    <div class="info">
+                        <div class="key">项目名称:</div>
+                        <div class="value">{{ item.name }}</div>
+                    </div>
+                    <div class="info">
+                        <div class="key">项目编号:</div>
+                        <div class="value">{{ item.id }}</div>
+                    </div>
+                </div>
+                <div class="info2">
+                    <div class="info">
+                        <div class="key">打卡星期:</div>
+                        <div class="value">{{ getWeekday(item.day_of_week)}}</div>
+                    </div>
+                    <div class="info">
+                        <div class="key">每周循环:</div>
+                        <div class="value">{{ item.loopy ? '是' : '否' }}</div>
+                    </div>
+                </div>
+                <div class="info">
+                    <div class="key">打卡地点:</div>
+                    <div class="value">{{ item.address }}</div>
+                </div>
+                <div class="info">
+                    <div class="key">打卡时间:</div>
+                    <div class="value">{{ item.begintime }}~{{ item.endtime }}</div>
+                </div>
+                <div class="action">
+                    <el-tag type="warning" size="large" style="margin-right: 5px" v-if="!hasRecord(item)">未打卡</el-tag>
+                    <el-tag type="success" size="large" style="margin-right: 5px" v-else>已打卡</el-tag>
+                    <el-tag type="danger" size="large" v-if="!isWithinAttendanceTime(item)">不在考勤时间内</el-tag>
+                    <el-tag type="success" size="large" v-else>正在考勤</el-tag>
+                </div>
+            </div>
+        </div>
+    </div>
+    <el-backtop :right="70" :bottom="70" />
+    <Footer />
+</template>
+
+<script setup>
+import Header from '../../components/Header.vue';
+import Footer from '../../components/Footer.vue';
+import { App } from '../../app/app';
+import { ServerAPI } from '../../app/lib/ServerAPI';
+import { useRouter } from 'vue-router'
+
+let data = ref([]);
+let records = ref([]);
+const router = useRouter();
+
+function onload() {
+    document.documentElement.scrollTop = 0;
+    if (!App.hasUser()) {
+        ElMessage.error('登录已过期,请重新登录');
+        return router.push('/login');
+    }
+
+    const loading = ElLoading.service({
+        lock: true,
+        text: '正在加载中,请稍候'
+    })
+
+    try{
+        ServerAPI.GetMyAttendanceItems(App.user.uuid, App.user.session, (res) => {
+            if (!res || res.code !== 0) {
+                loading.close();
+                return ElMessage.error(`获取考勤数据失败!${res.msg}`);
+            }
+            data.value = res.list;
+            records.value = res.records;
+            loading.close();
+        })
+    } catch(err) {
+        loading.close();
+        ElMessage.error(`获取考勤列表失败!${err}`)
+    }
+}
+onload();
+
+function getWeekday(day_of_week) {
+    switch (day_of_week) {
+        case 0:
+            return '星期日'
+        case 1:
+            return '星期一'
+        case 2:
+            return '星期二'
+        case 3:
+            return '星期三'
+        case 4:
+            return '星期四'
+        case 5:
+            return '星期五'
+        case 6:
+            return '星期六'
+
+        default:
+            return '未知星期'
+    }
+}
+
+function isWithinAttendanceTime(attendanceTimes) {
+    const now = new Date();
+    const dayOfWeek = now.getDay();
+    const nowTime = now.getTime();
+
+    function timeToTodayTimestamp(timeStr) {
+        const [hours, minutes, seconds] = timeStr.split(':').map(Number);
+        const todayWithTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, seconds);
+        return todayWithTime.getTime();
+    }
+
+    const { createTime, day_of_week, begintime, endtime, loopy } = attendanceTimes
+    if (dayOfWeek === day_of_week && (loopy || (Number(createTime) + 604800000 - nowTime > 0)) && timeToTodayTimestamp(begintime) <= nowTime && timeToTodayTimestamp(endtime) >= nowTime) {
+        return true;
+    }
+    return false
+}
+
+function hasRecord(attendanceData) {
+    const { id, day_of_week, begintime, loopy } = attendanceData;
+    if (!loopy) {
+        return records.value.some(record => id === record.project_id && record.uuid === App.user.uuid);
+    }
+    const now = new Date();
+    const dayOfWeek = now.getDay();
+    const nowTime = now.getTime();
+
+    function getTimestamp() {
+        // 计算今天与目标星期几的差值
+        // 目标星期几应当是从0到6的范围,0是星期日,6是星期六
+        const daysUntilTarget = (day_of_week - dayOfWeek + 7) % 7;
+        // 计算目标日期
+        const targetDate = new Date(now);
+        targetDate.setDate(now.getDate() + daysUntilTarget);
+        const [hours, minutes, seconds] = begintime.split(':').map(Number);
+        targetDate.setHours(hours, minutes, seconds, 0);
+        return targetDate.getTime();
+    }
+
+    const begin = getTimestamp();
+    if (nowTime >= begin) {
+        return records.value.some(record => id === record.project_id && record.time >= begin && record.uuid === App.user.uuid);
+    }
+
+    return records.value.some(record => id === record.project_id && record.time >= begin - 604800000 && record.uuid === App.user.uuid);
+}
+</script>
+
+<style scoped>
+.content {
+    width: 70%;
+    margin: 0 auto;
+    border-radius: 10px;
+    margin-top: 10px;
+    min-height: 70vh;
+}
+
+.content .item {
+    padding: 16px;
+    background-color: #fff;
+    border-radius: 10px;
+    margin: 0 auto;
+    margin-bottom: 10px;
+    width: 80%;
+}
+
+.content .item .info {
+    display: flex;
+    font-size: 0.9em;
+    align-items: flex-start;
+    margin-top: 5px;
+}
+
+.content .item .info2 {
+    display: flex;
+    align-items: flex-start;
+}
+
+.content .item .info2 .info {
+    flex: 1;
+    align-items: flex-start;
+}
+
+.content .item .info .key {
+    width: 75px;
+}
+
+.content .item .info .value {
+    color: #777;
+    flex: 1;
+}
+
+.content .item .info .msgValue {
+    color: #777;
+    display: flex;
+    flex-direction: column;
+}
+
+.content .item .info .value .nameItem {
+    margin-right: 5px;
+}
+
+.action {
+    margin-top: 10px;
+    display: flex;
+    justify-content: end;
+    height: 30px;
+}
+
+@media only screen and (max-width: 768px) {
+    .content {
+        width: 90%;
+        padding: 0;
+    }
+
+    .content .item {
+        width: 90%;
+    }
+
+}
+</style>

+ 132 - 0
src/pages/ClockIn/components/MapContainer.vue

@@ -0,0 +1,132 @@
+<template>
+  <div id="container"></div>
+</template>
+
+<script setup>
+import { onMounted, onUnmounted, ref, defineEmits } from "vue";
+import AMapLoader from "@amap/amap-jsapi-loader";
+
+let map = null;
+let position = ref();
+
+let props = defineProps({
+  position: {
+    type: Object,
+    default: () => ({})
+  },
+  radius: {
+    type: Number,
+    default: () => (50)
+  },
+  address: {
+    type: String,
+    default: () => ('打卡点')
+  }
+});
+
+const emit = defineEmits(['update','err']);
+
+onMounted(() => {
+  const loading = ElLoading.service({
+    lock: true,
+    text: '正在定位中,请稍候'
+  })
+
+  AMapLoader.load({
+    key: "d1f123693def8a412c976184daa4b60e",
+    version: "2.0", 
+  })
+    .then((AMap) => {
+      map = new AMap.Map("container", {
+        viewMode: '2D', //地图模式
+        zoom: 18, //初始化地图层级
+        center: props.position,
+      });
+
+      AMap.plugin(["AMap.ToolBar", "AMap.Scale", "AMap.Geolocation", "AMap.CircleEditor", "AMap.Geocoder"], function () {
+        var geolocation = new AMap.Geolocation({
+          enableHighAccuracy: true,
+          timeout: 10000, 
+          buttonPosition: 'RB',    //定位按钮的停靠位置
+          buttonOffset: new AMap.Pixel(10, 20),//定位按钮与设置的停靠位置的偏移量,默认:Pixel(10, 20)
+          zoomToAccuracy: true,
+          useNative: true,
+          GeoLocationFirst:true
+        });
+        map.addControl(geolocation);
+
+        //绘制签到范围
+        var circle = new AMap.Circle({
+          center: props.position,
+          radius: props.radius, //签到范围半径
+          borderWeight: 1,
+          strokeOpacity: 1,
+          strokeOpacity: 0.2,
+          fillOpacity: 0.4,
+        })
+
+        circle.setMap(map)
+        // 缩放地图到合适的视野级别
+        map.setFitView([circle])
+        new AMap.CircleEditor(map, circle)
+
+        const window = `<small>打卡点</small>`;
+        const infoWindow = new AMap.InfoWindow({
+          content: window,
+          anchor: "top-left",
+        });
+        infoWindow.open(map, props.position);
+
+        //临时定位方式 后续换为精度更高的企业微信定位
+        geolocation.getCurrentPosition(function (status, result) {
+          if (status == 'complete') {
+            position.value = result.position;
+            var distance = AMap.GeometryUtil.distance(result.position, props.position).toFixed(2);
+            emit('update', distance);
+            loading.close();
+          } else {
+            onError(result);
+            loading.close();
+          }
+        })
+
+        let toolbar = new AMap.ToolBar();
+        map.addControl(toolbar);
+        let scale = new AMap.Scale();
+        map.addControl(scale);
+      });
+
+      const marker = new AMap.Marker({
+        position: props.position,
+        title: "打卡点",
+      });
+      map.add(marker);
+    })
+    .catch((e) => {
+      console.log(e);
+    });
+
+});
+
+//解析定位错误信息
+function onError(data) {
+  alert(data.message);
+}
+
+onUnmounted(() => {
+  map?.destroy();
+});
+</script>
+
+<style scoped>
+#container {
+  width: 100%;
+  height: 450px;
+}
+
+@media only screen and (max-width: 768px) {
+  #container {
+    height: 400px;
+  }
+}
+</style>