Browse Source

Merge branch 'dev' of Pchen0/Double_X_Attendance_Web into master

Pchen0 7 months ago
parent
commit
d991a5ca8d

+ 0 - 6
README.md

@@ -2,13 +2,7 @@
 
 ## 🖥项目简介
 
-Double_X 考勤系统基于现代化的前端和后端技术栈开发,提供了灵活、智能的考勤管理解决方案。该系统主要功能包括单次打卡和每周循环打卡两种模式,用户可以根据需求自行设置打卡时段和周期,并支持补卡,满足不同场景下的考勤需求。
 
-前端技术栈采用 Vue 3、Vue Router 和 ElementPlus,带来了流畅的用户体验和高效的界面交互。通过集成高德地图 JS API,用户可以方便地在地图上选择考勤地点,并自定义考勤范围,实现精准定位和灵活管理。
-
-后端技术栈基于 Node.js 和 Express 搭建,配合 MySQL 数据库,提供了稳定、高效的数据存储和管理功能。系统具备强大的数据处理能力,助力用户更好地管理团队考勤。
-
-Double_X 考勤系统支持多种考勤模式和灵活配置,致力于为用户提供便捷、智能的考勤解决方案。
 
 ## 📦项目地址
 

+ 17 - 2
src/app/app.js

@@ -12,14 +12,14 @@ class User {
 
     static getLocalUser() {
         try {
-            let user = User.formatUser(JSON.parse(window.localStorage['user']));
+            let user = User.formatLocalUser(JSON.parse(window.localStorage['user']));
             return user;
         } catch (e) {
             return undefined;
         }
     }
 
-    static formatUser(data) {
+    static formatLocalUser(data) {
         try {
             let user = new User();
             user.uuid = data.uuid;
@@ -34,6 +34,21 @@ class User {
         }
     }
 
+    static formatUser(data) {
+        try {
+            let user = new User();
+            user.uuid = data.uuid;
+            user.session = data.session;
+            user.username = data.username;
+            user.email = data.email;
+            user.wxid = data.wxid;
+            user.avatar = import.meta.env.VITE_API_URL + data.avatar;
+            return user;
+        } catch (e) {
+            return undefined;
+        }
+    }
+
     static setLocalUser(user) {
         window.localStorage['user'] = JSON.stringify(user);
     }

+ 25 - 2
src/app/lib/ServerAPI.js

@@ -72,9 +72,9 @@ class ServerAPI {
         )
     }
 
-    static UpdateInfo(uuid, session, code, callback = function () { }) {
+    static BindWXWork(uuid, session, code, callback = function () { }) {
         core.request(
-            "/User/UpdateInfo",
+            "/User/BindWXWork",
             {
                 uuid,
                 session,
@@ -85,6 +85,29 @@ class ServerAPI {
         )
     }
 
+    static BindEmail(uuid, session, email, code, callback = function () { }) {
+        core.request(
+            "/User/BindEmail",
+            {
+                uuid,
+                session,
+                email,
+                code
+            },
+            { method: 'POST' },
+            callback
+        )
+    }
+
+    static ChangePassword(uuid, session, oldpassword, password, callback = function () { }) {
+        core.request(
+            "/User/ChangePassword",
+            { uuid, session, oldpassword, password },
+            { method: 'POST' },
+            callback
+        )
+    }
+
     static getPermissions(uuid, session, callback = function () { }) {
         core.request(
             "/User/GetPermissions",

+ 3 - 6
src/components/Header.vue

@@ -26,11 +26,11 @@
                         </el-icon>
                         我的考勤
                     </div>
-                    <div class="item" @click="push('https://git.vthc.cn/Pchen0/Double_X_Attendance')">
+                    <div class="item" @click="$router.push('/Setup')">
                         <el-icon>
-                            <FolderOpened />
+                            <Setting />
                         </el-icon>
-                        代码仓库
+                        设置中心
                     </div>
                     <div class="item" v-if="app.user === undefined" @click="$router.push('/login')">
                         <el-icon>
@@ -110,9 +110,6 @@ if (user != undefined) {
     })
 }
 
-const push = (url) => {
-    window.open(url);
-}
 </script>
 
 <style scoped>

+ 2 - 1
src/pages/Admin/components/UserList.vue

@@ -4,7 +4,7 @@
         <el-table-column label="用户名">
             <template #default="scope">
                 <div style="display: flex; gap: 5px">
-                    <el-avatar :src="scope.row.avatar" :size="25" />
+                    <el-avatar :src="apiUrl + scope.row.avatar" :size="25" />
                     {{ scope.row.username }}
                 </div>
             </template>
@@ -28,6 +28,7 @@ import { App } from '../../../app/app';
 import { ServerAPI } from '../../../app/lib/ServerAPI';
 
 let users = ref([]);
+let apiUrl = import.meta.env.VITE_API_URL;
 
 onMounted(() => {
     ServerAPI.UserList(App.user.uuid, App.user.session, (r) => {

+ 8 - 11
src/pages/ClockIn/ClockIn.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="header">
-        <Header :color="'black'"/>
+        <Header :color="'black'" />
     </div>
 
     <div class="root">
@@ -83,7 +83,7 @@
                     <div class="key">发起人:</div>
                     <div class="value2">
                         <div class="useritem">
-                            <el-avatar :size="23" :src="userInfo[data.createUser].avatar" />
+                            <el-avatar :size="23" :src="apiUrl + userInfo[data.createUser].avatar" />
                             {{ userInfo[data.createUser] !=
                             undefined ? userInfo[data.createUser].username : `用户${data.createUser.slice(0, 4)}` }}
                         </div>
@@ -95,7 +95,7 @@
                     <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" />
+                            <el-avatar :size="23" :src="apiUrl + userInfo[item].avatar" />
                             {{ userInfo[item] !=
                             undefined ? userInfo[item].username : `用户${item.slice(0, 4)}` }}
                         </div>
@@ -106,7 +106,7 @@
                     <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" />
+                            <el-avatar :size="23" :src="apiUrl + userInfo[item].avatar" />
                             {{ userInfo[item] !=
                             undefined ? userInfo[item].username : `用户${item.slice(0, 4)}` }}
                         </div>
@@ -117,7 +117,7 @@
                     <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" />
+                            <el-avatar :size="23" :src="apiUrl + userInfo[item].avatar" />
                             {{ userInfo[item] !=
                             undefined ? userInfo[item].username : `用户${item.slice(0, 4)}` }}
                         </div>
@@ -129,7 +129,7 @@
                         <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" />
+                            <el-avatar :size="23" :src="apiUrl + userInfo[item.uuid].avatar" />
                             {{ userInfo[item.uuid] !=
                             undefined ? userInfo[item.uuid].username : `用户${item.uuid.slice(0, 4)}` }}
                         </div>
@@ -163,7 +163,7 @@
         </div>
     </div>
 
-    <el-backtop :right="70" :bottom="70"/>
+    <el-backtop :right="70" :bottom="70" />
     <Footer />
 </template>
 
@@ -189,6 +189,7 @@ let records = ref([]);
 let thisWeekRecords = ref([]);
 let showAll = ref(false);
 let noRecord = ref([]);
+let apiUrl = import.meta.env.VITE_API_URL;
 
 function Update(data) {
     distance.value = data;
@@ -249,10 +250,6 @@ function onload() {
             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
     })
 }

+ 1 - 1
src/pages/Login/components/ForgetPassword.vue

@@ -208,7 +208,7 @@ let Register = () => {
 @media only screen and (max-width: 768px) {
     .content {
         width: 80%;
-        height: 60%;
+        height: 550px;
         padding: 25px;
     }
 

+ 2 - 1
src/pages/Login/components/Index.vue

@@ -7,6 +7,7 @@
             <span @click="update('register')">注册账号</span> |
             <span @click="update('forget')">忘记密码</span>
         </div>
+        <router-view />
     </div>
 </template>
 
@@ -75,7 +76,7 @@ let WXLogin = function () {
 @media only screen and (max-width: 768px) {
     .content {
         width: 80%;
-        height: 35%;
+        height: 300px;
         padding: 25px;
     }
 

+ 1 - 1
src/pages/Login/components/PasswordLogin.vue

@@ -168,7 +168,7 @@ let Login = () => {
 @media only screen and (max-width: 768px) {
     .content {
         width: 80%;
-        height: 45%;
+        height: 400px;
         padding: 25px;
     }
 

+ 1 - 1
src/pages/Login/components/Register.vue

@@ -214,7 +214,7 @@ let Register = () => {
 @media only screen and (max-width: 768px) {
     .content {
         width: 80%;
-        height: 60%;
+        height: 550px;
         padding: 25px;
     }
 

+ 5 - 6
src/pages/Main/Main.vue

@@ -15,18 +15,17 @@
             </el-icon>
             <span>我的考勤</span>
           </div>
-          <div class="button"
-            @click="push('https://git.vthc.cn/Pchen0/Double_X_Attendance/src/master/README.md')">
+          <div class="button" @click="push('https://git.vthc.cn/Pchen0/Double_X_Attendance/src/master/README.md')">
             <el-icon :size="35">
               <List />
             </el-icon>
             <span>操作文档</span>
           </div>
-          <div class="button" @click="push('https://git.vthc.cn/Pchen0/Double_X_Attendance.git')">
+          <div class="button" @click="$router.push('/Setup')">
             <el-icon :size="35">
-              <Lollipop />
+              <Tools />
             </el-icon>
-            <span>代码仓库</span>
+            <span>设置中心</span>
           </div>
           <div class="button" @click="$router.push('/Mine')">
             <el-icon :size="35">
@@ -55,7 +54,7 @@
 </template>
 
 <script setup>
-import { onMounted, provide} from 'vue'
+import { onMounted, provide } from 'vue'
 import { useRoute } from 'vue-router'
 import Header from '../../components/Header.vue'
 import Footer from '../../components/Footer.vue'

+ 3 - 3
src/pages/Mine/Mine.vue

@@ -2,7 +2,7 @@
     <Header />
     <div style="min-height: 75vh;">
         <div class="cards">
-            <div class="card" v-if="app.user != undefined">
+            <div class="card" v-if="app.user != undefined" @click="$router.push('/UpdateInfo')">
                 <div class="top">
                     <div class="avatar">
                         <el-avatar :src="app.user.avatar" v-if="app.user.avatar" :size="avatarSize" />
@@ -60,11 +60,11 @@
                         <span>网站管理</span>
                     </div>
 
-                    <div class="button" @click="$router.push('/UpdateInfo')">
+                    <div class="button" @click="$router.push('/Setup')">
                         <el-icon :size="30">
                             <Setting />
                         </el-icon>
-                        <span>更新个人信息</span>
+                        <span>设置中心</span>
                     </div>
 
                     <div class="button" @click="logout()" v-if="app.user !== undefined">

+ 151 - 0
src/pages/Setup/ChangePassword/ChangePassword.vue

@@ -0,0 +1,151 @@
+<template>
+    <Header />
+
+    <div class="root">
+        <div class="content">
+            <h1>修改密码</h1>
+            <el-form :model="form" ref="formRef">
+                <el-form-item prop="oldpassword">
+                    <el-input v-model="form.oldpassword" placeholder="输入旧密码" type="password" :prefix-icon="Lock" />
+                </el-form-item>
+
+                <el-form-item prop="password">
+                    <el-input v-model="form.password" placeholder="输入新密码" type="password" :prefix-icon="Lock" />
+                </el-form-item>
+
+                <el-form-item prop="password1">
+                    <el-input v-model="form.password1" placeholder="再次输入新密码" type="password" :prefix-icon="Lock" />
+                </el-form-item>
+            </el-form>
+            <button @click="Submit()">提交</button>
+        </div>
+
+    </div>
+
+    <Footer />
+</template>
+
+<script setup>
+import Header from '../../../components/Header.vue';
+import Footer from '../../../components/Footer.vue';
+import { App } from '../../../app/app';
+import { useRouter } from 'vue-router';
+import { ServerAPI } from '../../../app/lib/ServerAPI';
+import { Lock } from '@element-plus/icons-vue';
+
+let router = useRouter();
+
+if (!App.hasUser()) {
+    ElMessage.error('请登录!')
+    router.replace({
+        name: "Login"
+    });
+}
+
+let formRef = ref();
+let form = reactive({
+    oldpassword: '',
+    password: '',
+    password1: ''
+});
+
+function CheckPassword(password) {
+    if (password.length < 8 || password.length > 16) {
+        return false;
+    }
+
+    // 使用正则表达式检查密码是否包含至少一个字母和一个数字
+    const hasLetter = /[a-zA-Z]/.test(password); // 检查是否包含字母
+    const hasNumber = /\d/.test(password);       // 检查是否包含数字
+
+    return hasLetter && hasNumber;
+}
+
+let Submit = () => {
+    if (form.oldpassword === '' ||form.password === '' || form.password1 === '')
+        return ElMessage.error('请确保每项不能为空');
+    if (!CheckPassword(form.password1))
+        return ElMessage.error('密码需在8到16位之间,且包含字母和数字');
+    if (form.password1 !== form.password)
+        return ElMessage.error('请确保两次输入的密码一致');
+
+    let oldpassword = btoa(form.oldpassword);
+    let password = btoa(form.password);
+
+    ServerAPI.ChangePassword(App.user.uuid, App.user.session, oldpassword, password, (res) => {
+        if (!res || res.code !== 0)
+            return ElMessage.error(`修改失败!${res.msg || ''}`);
+
+        ElMessage.success('修改成功!');
+    });
+}
+
+</script>
+
+<style scoped>
+.root {
+    min-height: 75vh;
+}
+
+.content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+    background-color: rgba(255, 255, 255, 0.7);
+    position: absolute;
+    top: 50%;
+    left: 50%;   transform: translate(-50%, -50%);
+    height: 350px;
+    width: 650px;
+    box-sizing: border-box;
+    border-radius: 10px;
+}
+
+.content .input {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+.content h1 {
+    color: #337ecc;
+    font-size: 1.6em;
+    margin: 0;
+    margin-bottom: 20px;
+}
+
+.el-input {
+    border-radius: 15px;
+    height: 50px;
+}
+
+.content button {
+    width: 100px;
+    height: 35px;
+    margin-top: 5px;
+    font-size: 1em;
+    border: none;
+    border-radius: 10px;
+    background-color: #337ecc;
+    color: #fff;
+}
+
+@media only screen and (max-width: 768px) {
+    .content {
+        width: 80%;
+        height: 350px;
+        padding: 25px;
+    }
+
+    .el-input {
+        height: 40px;
+    }
+
+    .content button {
+        height: 35px;
+        width: 150px;
+        font-size: 0.9em;
+    }
+}
+</style>

+ 145 - 0
src/pages/Setup/Setup.vue

@@ -0,0 +1,145 @@
+<template>
+    <Header />
+    <div class="mainRoot">
+        <div class="settings">
+            <router-link to="/UpdateInfo">
+                <div class="item">
+                    <div class="icon">
+                        <span class="iconfont">
+                            <el-icon>
+                                <Refresh />
+                            </el-icon>
+                        </span>
+                    </div>
+                    <div class="name">个人信息管理</div>
+                    <div class="btn">
+                        <span class="iconfont">
+                            <el-icon><arrow-right /></el-icon>
+                        </span>
+                    </div>
+                </div>
+            </router-link>
+
+            <router-link to="/ChangePassword">
+                <div class="item">
+                    <div class="icon">
+                        <span class="iconfont">
+                            <el-icon>
+                                <Lock />
+                            </el-icon>
+                        </span>
+                    </div>
+                    <div class="name">修改密码</div>
+                    <div class="btn">
+                        <span class="iconfont">
+                            <el-icon><arrow-right /></el-icon>
+                        </span>
+                    </div>
+                </div>
+            </router-link>
+
+            <div class="item end" @click="onDevelopment">
+                <div class="icon">
+                    <span class="iconfont">
+                        <el-icon>
+                            <QuestionFilled />
+                        </el-icon>
+                    </span>
+                </div>
+                <div class="name">帮助与反馈</div>
+                <div class="btn">
+                    <span class="iconfont">
+                        <el-icon><arrow-right /></el-icon>
+                    </span>
+                </div>
+            </div>
+
+        </div>
+
+    </div>
+</template>
+
+<script setup>
+import Header from '../../components/Header.vue'
+import { useRouter } from 'vue-router';
+import { App } from '../../app/app';
+import { QuestionFilled } from '@element-plus/icons-vue';
+
+let router = useRouter();
+
+if (!App.hasUser()) {
+    ElMessage({
+        message: "请登录",
+        type: 'warning',
+    });
+    router.replace({
+        name: "Login"
+    });
+}
+
+</script>
+
+<style scoped>
+.settings {
+    width: 70%;
+    margin: 0 auto;
+    padding: 12px;
+    background-color: #fff;
+    border-radius: 12px;
+    display: flex;
+    flex-direction: column;
+}
+
+.settings .item {
+    display: flex;
+    padding-top: 8px;
+    padding-bottom: 8px;
+    align-items: center;
+    border-bottom: 1px solid #eee;
+}
+
+.settings .item.end {
+    border-bottom: none;
+}
+
+.settings .item .icon {
+    width: 30px;
+    height: 30px;
+    /* border: 1px solid red; */
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.settings .item .icon .iconfont {
+    font-size: 20px;
+}
+
+.settings .item .name {
+    margin-left: 12px;
+    flex: 1;
+}
+
+.logoutBtn {
+    height: 45px;
+    background-color: #c8161d;
+    border-radius: 10px;
+    margin-left: 18px;
+    margin-right: 18px;
+    margin-bottom: 18px;
+    margin-top: 20px;
+    line-height: 45px;
+    text-align: center;
+    color: #fff;
+    font-weight: bold;
+    font-size: 16px;
+}
+
+@media only screen and (max-width: 768px) {
+    .settings {
+        margin-top: 10px;
+        width: 80%;
+    }
+
+}
+</style>

+ 0 - 117
src/pages/Setup/UpdateInfoStage1.vue

@@ -1,117 +0,0 @@
-<template>
-    <Header />
-    <div class="root">
-        <div class="content">
-            <div class="avatar"><el-avatar :size="100" :src="App.user.avatar ? App.user.avatar : ''" /></div>
-
-            <el-form :model="form" ref="formRef" label-width="auto" :label-position="'right'" style>
-                <el-form-item label="用户名" prop="username">
-                    <el-input v-model="form.username" disabled />
-                </el-form-item>
-
-                <el-form-item label="企业微信ID" prop="wxid">
-                    <el-input v-model="form.wxid" disabled />
-                </el-form-item>
-            </el-form>
-
-            <div class="btn" @click="onGetUrl">
-                获取企业微信资料
-            </div>
-
-            <div class="agreementBox">
-                <div class="text">
-                    我们将获取你的用户名、用户ID等信息
-                </div>
-            </div>
-        </div>
-
-        <router-view></router-view>
-    </div>
-    <Footer />
-</template>
-
-<script setup>
-import Header from '../../components/Header.vue';
-import Footer from '../../components/Footer.vue';
-import { App } from '../../app/app';
-import { useRouter } from 'vue-router';
-import { ServerAPI } from '../../app/lib/ServerAPI';
-
-let router = useRouter();
-
-if (!App.hasUser()) {
-    ElMessage.error('请登录!')
-    router.replace({
-        name: "Login"
-    });
-}
-
-let formRef = ref();
-let form = reactive({
-    username: App.user.username,
-    wxid: App.user.wxid
-});
-
-
-let onGetUrl = function () {
-    ServerAPI.WXWorkUrl(0, 'update', (res) => {
-        if (res == undefined || res.code != 0) 
-            return ElMessage.error(`获取登录链接失败!${res.msg}`);
-        
-        window.location.href = res.data.url
-    });
-}
-</script>
-
-<style scoped>
-.root {
-    min-height: 70vh;
-}
-
-.avatar {
-    margin-top: -20px;
-    margin-bottom: 20px;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-}
-
-.content {
-    padding: 18px;
-    width: 60%;
-    margin: 0 auto;
-}
-
-.btn {
-    margin-top: 10px;
-    margin-bottom: 5px;
-    width: 100%;
-    height: 50px;
-    background-color: #337ecc;
-    border-radius: 10px;
-    line-height: 50px;
-    text-align: center;
-    color: #fff;
-    font-weight: bold;
-    font-size: 14px;
-}
-
-.agreementBox .text {
-    margin-left: 5px;
-    display: inline;
-    font-size: 12px;
-    color: #777;
-    line-height: 20px;
-}
-
-.agreementBox .text .bold {
-    font-weight: bold;
-    color: #000;
-}
-
-@media only screen and (max-width: 768px) {
-    .content {
-        width: 90%
-    }
-}
-</style>

+ 204 - 0
src/pages/Setup/UserInfo/BindEmail.vue

@@ -0,0 +1,204 @@
+<template>
+    <Header />
+
+    <div class="root">
+        <div class="content">
+            <h1>更换绑定邮箱</h1>
+            <el-form :model="form" ref="formRef">
+                <el-form-item prop="email">
+                    <el-input v-model="form.email" placeholder="输入新邮箱" :prefix-icon="Postcard" />
+                </el-form-item>
+
+                <el-form-item prop="text">
+                    <div class="captcha">
+                        <el-input v-model="form.text" placeholder="输入验证码" class="captcha-input"
+                            :prefix-icon="Finished" />
+                        <img alt="验证码" :src="ImageCaptcha" @click="getCaptcha">
+                    </div>
+                </el-form-item>
+
+                <el-form-item prop="code">
+                    <el-input v-model="form.code" placeholder="邮箱验证码" :prefix-icon="Finished">
+                        <template #append>
+                            <div @click="sendEmail" v-if="time === 0">获取验证码</div>
+                            <div disabled v-else>{{ time }} s</div>
+                        </template>
+                    </el-input>
+                </el-form-item>
+            </el-form>
+            <button @click="Submit()">提交</button>
+        </div>
+
+    </div>
+
+    <Footer />
+</template>
+
+<script setup>
+import Header from '../../../components/Header.vue';
+import Footer from '../../../components/Footer.vue';
+import { App } from '../../../app/app';
+import { useRouter } from 'vue-router';
+import { ServerAPI } from '../../../app/lib/ServerAPI';
+import {  Postcard, Finished } from '@element-plus/icons-vue';
+
+let router = useRouter();
+
+if (!App.hasUser()) {
+    ElMessage.error('请登录!')
+    router.replace({
+        name: "Login"
+    });
+}
+
+let ImageCaptcha = ref('');
+let time = ref(0);
+let disabled = ref(false);
+let formRef = ref();
+let form = reactive({
+    email: App.user.email,
+    text: '',
+    code: ''
+});
+
+let getCaptcha = () => {
+    ServerAPI.ImageCaptcha((r) => {
+        if (!r || r.code != 0)
+            ElMessage.error(`获取图片验证码失败!${r.msg}`);
+
+        ImageCaptcha.value = r.data.img;
+        form.id = r.data.id;
+    })
+}
+
+onMounted(() => {
+    getCaptcha()
+})
+
+let settime = () => {
+    time.value = 60;
+    const timer = setInterval(() => {
+        if (time.value > 0) {
+            time.value--;
+        } else {
+            clearInterval(timer)
+        }
+    }, 1000)
+}
+
+let sendEmail = () => {
+    if (disabled.value)
+        return ElMessage.warning('您的操作太快了,休息一下吧!');
+    if (form.email == '')
+        return ElMessage.error('请填写邮箱');
+    if (form.text == '')
+        return ElMessage.error('请填写图片验证码');
+
+    disabled.value = true;
+    ServerAPI.SendEmail(form.email, form.text, form.id, 'bind', (r) => {
+        if (r && r.code === 0) {
+            ElMessage.success('发送邮箱验证码成功!');
+            settime();
+            disabled.value = false;
+        } else {
+            ElMessage.error(`获取邮箱验证码失败!${r.msg || ''}`);
+            disabled.value = false;
+        }
+    })
+}
+
+let Submit = () => {
+    if (form.email === '' || form.code === '' || form.text === '')
+        return ElMessage.error('请确保每项不能为空');
+
+    ServerAPI.BindEmail(App.user.uuid, App.user.session, form.email, form.code, (res) => {
+        if (!res || res.code !== 0){
+            getCaptcha();
+            return ElMessage.error(`绑定失败!${res.msg || ''}`);
+        }
+           
+        App.refershUser(res.data);
+        ElMessage.success('绑定成功!');
+        router.push('/UpdateInfo');
+    });
+}
+
+</script>
+
+<style scoped>
+.root {
+    min-height: 75vh;
+}
+
+.content {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+    background-color: rgba(255, 255, 255, 0.7);
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    height: 350px;
+    width: 650px;
+    box-sizing: border-box;
+    border-radius: 10px;
+}
+
+.content .input {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+.content h1 {
+    color: #337ecc;
+    font-size: 1.6em;
+    margin: 0;
+    margin-bottom: 20px;
+}
+
+.el-input {
+    border-radius: 15px;
+    height: 50px;
+}
+
+.content .captcha {
+    display: flex;
+    height: 50px;
+}
+
+.content button {
+    width: 180px;
+    height: 40px;
+    margin-top: 10px;
+    font-size: 1em;
+    border: none;
+    border-radius: 10px;
+    background-color: #337ecc;
+    color: #fff;
+}
+
+@media only screen and (max-width: 768px) {
+    .content {
+        width: 80%;
+        height: 350px;
+        padding: 25px;
+    }
+
+    .el-input {
+        height: 40px;
+    }
+
+    .content .captcha {
+        height: 40px;
+    }
+
+    .content button {
+        height: 35px;
+        width: 150px;
+        font-size: 0.9em;
+    }
+}
+</style>

+ 11 - 15
src/pages/Setup/UpdateInfoStage2.vue → src/pages/Setup/UserInfo/BindWXWork.vue

@@ -1,7 +1,7 @@
 <script setup>
 import { useRouter } from 'vue-router';
-import { App } from '../../app/app';
-import { ServerAPI } from '../../app/lib/ServerAPI';
+import { App } from '../../../app/app';
+import { ServerAPI } from '../../../app/lib/ServerAPI';
 
 let router = useRouter();
 
@@ -19,41 +19,37 @@ onMounted(async () => {
 
     if (num === 0 && code === '' || code === null) {
         num = 1
-        ServerAPI.WXWorkUrl(num, 'update', (res) => {
+        ServerAPI.WXWorkUrl(num, 'bind', (res) => {
             loading.close();
 
-            if (res == undefined || res.code != 0) 
-                return ElMessage.error(`更新失败!${res.msg}`)
+            if (res == undefined || res.code != 0)
+                return ElMessage.error(`更新失败!${res.msg || ''}`)
 
             window.location.href = res.data.url
         })
     }
 
     try {
-        ServerAPI.UpdateInfo(App.user.uuid, App.user.session, code,(res) => {
+        ServerAPI.BindWXWork(App.user.uuid, App.user.session, code, (res) => {
             if (res.code !== 0) {
                 loading.close();
-                ElMessage.error(`更新失败!${res.msg}`);
+                ElMessage.error(`绑定失败!${res.msg || ''}`);
                 router.replace({
-                    name: "UpdateInfoStage1",
+                    name: "UpdateInfo",
                 })
                 return;
             }
 
             loading.close();
-            ElMessage.success("更新个人信息成功");
+            ElMessage.success("绑定企业微信成功");
             App.refershUser(res.data);
-
         })
     } catch (error) {
         loading.close();
-        ElMessage({
-            message: "更新失败!请稍后再试" + error,
-            type: "error"
-        });
+        ElMessage.error(`绑定失败!${error.message || ''}`)
 
         router.replace({
-            name: "UpdateInfoStage1",
+            name: "UpdateInfo",
         })
     }
 });

+ 131 - 0
src/pages/Setup/UserInfo/UpdateInfoStage1.vue

@@ -0,0 +1,131 @@
+<template>
+    <Header />
+    <div class="root">
+        <div class="content">
+            <el-upload class="avatar" :action="apiUrl" :show-file-list="false" :on-success="handleAvatarSuccess"
+                :on-error="handleAvatarFail" :before-upload="beforeAvatarUpload" name="avatar">
+                <el-avatar :src="App.user.avatar" :size="130" />
+            </el-upload>
+
+            <el-form :model="form" ref="formRef" label-width="auto" :label-position="'right'" style>
+                <el-form-item label="用户名" prop="username">
+                    <el-input v-model="form.username" disabled />
+                </el-form-item>
+
+                <el-form-item label="邮箱" prop="email">
+                    <el-input v-model="form.email" disabled />
+                </el-form-item>
+
+                <el-form-item label="企业微信ID" prop="wxid">
+                    <el-input v-model="form.wxid" disabled />
+                </el-form-item>
+            </el-form>
+
+            <div class="btn" @click="BindWXWork">
+                绑定企业微信
+            </div>
+
+            <div class="btn" @click="$router.push('/UpdateInfo/BindEmail')">
+                更换绑定邮箱
+            </div>
+        </div>
+
+        <router-view></router-view>
+    </div>
+    <Footer />
+</template>
+
+<script setup>
+import Header from '../../../components/Header.vue';
+import Footer from '../../../components/Footer.vue';
+import { App } from '../../../app/app';
+import { useRouter } from 'vue-router';
+import { ServerAPI } from '../../../app/lib/ServerAPI';
+
+let router = useRouter();
+const apiUrl = import.meta.env.VITE_API_URL + `/User/UploadAvatar/${App.user.uuid}/${App.user.session}`;
+
+if (!App.hasUser()) {
+    ElMessage.error('请登录!')
+    router.replace({
+        name: "Login"
+    });
+}
+
+let formRef = ref();
+let form = reactive({
+    username: App.user.username,
+    wxid: App.user.wxid || '未绑定企业微信',
+    email: App.user.email
+});
+
+let BindWXWork = function () {
+    ServerAPI.WXWorkUrl(0, 'bind', (res) => {
+        if (res == undefined || res.code != 0) 
+            return ElMessage.error(`获取登录链接失败!${res.msg || ''}`);
+        window.location.href = res.data.url
+    });
+}
+
+const beforeAvatarUpload = (rawFile) => {
+    if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
+        ElMessage.error('请上传jpg或png格式的图片!');
+        return false
+    } else if (rawFile.size / 1024 / 1024 > 3) {
+        ElMessage.error('请上传大小不超过3MBd的头像!');
+        return false
+    }
+    return true
+}
+
+const handleAvatarSuccess = (res) => {
+    if(!res || res.code != 0)
+        return ElMessage.error(`头像上传失败!${res.msg || ''}`);
+    ElMessage.success('头像上传成功!');
+    App.refershUser(res.data);
+    router.go(0);
+}
+
+const handleAvatarFail = () => {
+    ElMessage.error('头像上传失败!请稍后再试');
+}
+</script>
+
+<style scoped>
+.root {
+    min-height: 75vh;
+}
+
+.avatar {
+    margin-bottom: 20px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.content {
+    padding: 18px;
+    width: 60%;
+    margin: 0 auto;
+}
+
+.btn {
+    margin-top: 10px;
+    margin-bottom: 5px;
+    width: 100%;
+    height: 50px;
+    background-color: #337ecc;
+    border-radius: 10px;
+    line-height: 50px;
+    text-align: center;
+    color: #fff;
+    font-weight: bold;
+    font-size: 14px;
+}
+
+@media only screen and (max-width: 768px) {
+    .content {
+        width: 90%
+    }
+}
+</style>

+ 24 - 7
src/router/index.js

@@ -50,21 +50,38 @@ const routes = [
         component: () => import('../pages/Admin/ClockIn/components/EditItem.vue'),
         meta: { title: '编辑考勤项目' }
     },
-    
+    {
+        path: "/Setup",
+        name: "Setup",
+        component: () => import('../pages/Setup/Setup.vue'),
+        meta: { title: '设置中心' }
+    },
     {
         name: "UpdateInfo",
         path: "/UpdateInfo",
-        component: () => import('../pages/Setup/UpdateInfoStage1.vue'),
+        component: () => import('../pages/Setup/UserInfo/UpdateInfoStage1.vue'),
         meta: { title: '更新个人信息' },
         children: [
             {
-                name: "UpdateInfoStage2",
-                path: "Stage2",
-                component: () => import('../pages/Setup/UpdateInfoStage2.vue'),
-                meta: { title: '更新个人信息' }
+                name: "BindWXWork",
+                path: "BindWXWork",
+                component: () => import('../pages/Setup/UserInfo/BindWXWork.vue'),
+                meta: { title: '绑定企业微信' }
             }
         ]
     },
+    {
+        name: "BindEmail",
+        path: "/UpdateInfo/BindEmail",
+        component: () => import('../pages/Setup/UserInfo/BindEmail.vue'),
+        meta: { title: '更换绑定邮箱' }
+    },
+    {
+        name: "ChangePassword",
+        path: "/ChangePassword",
+        component: () => import('../pages/Setup/ChangePassword/ChangePassword.vue'),
+        meta: { title: '修改密码' }
+    },
     {
         path: "/Admin",
         name: "Admin",
@@ -80,7 +97,7 @@ const router = VueRouter.createRouter({
 });
 
 router.beforeEach((to, from, next) => {
-    if (!to.meta.title) 
+    if (!to.meta.title)
         document.title = 'Double_X 考勤'
     else
         document.title = to.meta.title