Browse Source

管理员登录相关

pull/14/head
longchao 4 weeks ago
parent
commit
59f41e8122
  1. 1
      package.json
  2. 5
      src/App.vue
  3. 1
      src/assets/doctor_h5.svg
  4. 1
      src/assets/login_backImg.svg
  5. 117
      src/components/Header.vue
  6. 49
      src/router/index.js
  7. 12
      src/utils/request.js
  8. 31
      src/views/HomeView.vue
  9. 382
      src/views/login.vue
  10. 14
      src/views/paymentMethod.vue

1
package.json

@ -15,6 +15,7 @@
"regenerator-runtime": "^0.14.1",
"vue": "^2.6.14",
"vue-clickaway": "^2.2.2",
"vue-qr": "^4.0.9",
"vue-router": "^3.5.1",
"vuex": "^3.6.2"
},

5
src/App.vue

@ -11,7 +11,7 @@
<SliderMenu v-if="showSidebar" :menuData="slidermenu" :customize="customize"></SliderMenu>
<el-container class="main-right-content">
<!-- 面包屑导航 -->
<Breadcrumb />
<Breadcrumb v-if="user"/>
<el-main class="app-content">
<global-loading>
<router-view />
@ -34,6 +34,7 @@ export default {
data() {
return {
isCollapse: true,
user:null
};
},
components: {
@ -46,6 +47,8 @@ export default {
...mapState(['hosMenuData','slidermenu','customize','showSidebar', 'showFooter', 'showHeader']) // VuexshowSidebar
},
mounted() {
this.user = localStorage.getItem('nick')
window.addEventListener('beforeunload', this.clearStorage);
},
beforeUnmount() {

1
src/assets/doctor_h5.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 357 KiB

1
src/assets/login_backImg.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 MiB

117
src/components/Header.vue

@ -3,37 +3,121 @@
<div class="header-logo" @click="toHome">
<img src="../assets/header-logo.png" alt="">
</div>
<div class="header-right flex">
<!-- <div class="right-item">权限设置</div>
<div class="right-item">岗位设置</div> -->
<!-- <el-dropdown trigger="click">
<div class="header-right flex" v-if="headerShow">
<div
:class="['right-item', currentPath == item.path ? 'right-item-active' :'']"
v-for="(item,index) in routerNavList"
:key="index"
@click="goToPage(item.path)"
>
{{ item.name }}
</div>
<el-dropdown trigger="click">
<div class="right-item flex el-dropdown-link">
<img class="right-item-icon" src="../assets/header-icon.png" alt="">
<span>学术论文出版</span>
<img src="../assets/header-drop.svg" alt="">
<span>{{user}}</span>
<div class="img"></div>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-plus">黄金糕</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus">狮子头</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-plus-outline">螺蛳粉</el-dropdown-item>
<el-dropdown-item icon="el-icon-check">双皮奶</el-dropdown-item>
<el-dropdown-item icon="el-icon-circle-check">蚵仔煎</el-dropdown-item>
<el-dropdown-item><div @click="loginOut">退出</div></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown> -->
</el-dropdown>
</div>
</div>
</template>
<script>
export default {
data(){
return{
user: '',
headerShow:false,
doctor_id:'',
currentPath:'',
routerNavList:[
{
name:'个人信息',
path:'/doctorInformation'
},
{
name:'医院管理',
path:'/hospitalManage'
},
{
name:'收款管理',
path:'/paymentMethod'
},
]
}
},
mounted(){
this.user = localStorage.getItem('nick')
this.doctor_id = localStorage.getItem('doctor_id')
if(this.user){
this.headerShow = true
//
this.updateCurrentPath()
//
this.watchRouteChange()
}else{
this.headerShow = false
}
},
methods: {
toHome() {
this.$router.push({
path: '/'
})
},
goToPage(path) {
this.currentPath = path;
this.$router.push({
path: path + '?doctor_id=' + this.doctor_id
})
},
loginOut(){
// 退
this.$http('POST', '/login_out', {}).then(response => {
if(response.status){
// ---
localStorage.removeItem('token');
localStorage.removeItem('nick');
localStorage.removeItem('doctor_id');
// 退
this.$router.push('/login');
}else{
this.$message.error(response.data.info)
}
}).catch(error => {
console.error(error, 'error')
})
},
//
updateCurrentPath() {
const currentRoute = this.$route.path;
const matchedItem = this.routerNavList.find(item =>
currentRoute.startsWith(item.path)
);
if (matchedItem) {
this.currentPath = matchedItem.path;
} else {
this.currentPath = '';
}
},
//
watchRouteChange() {
this.$watch('$route', (to, from) => {
console.log(to,from,'====');
this.updateCurrentPath();
});
}
},
beforeDestroy() {
}
}
</script>
<style lang="scss" scoped>
.flex {
display: flex;
@ -56,8 +140,8 @@ export default {
}
.header-right {
width: 362px;
justify-content: flex-end;
gap:20px;
.el-dropdown-link {
cursor: pointer;
}
@ -72,12 +156,17 @@ export default {
transition: all .3s;
}
}
.right-item-active{
border-radius: 6px;
background: rgba(255, 255, 255, 0.4);
color: #006AFF;
transition: all .3s;
}
.el-dropdown .right-item{
padding: 0 10px;
cursor: pointer;
}
.right-item-icon {
width: 36px;
height: 36px;

49
src/router/index.js

@ -5,10 +5,19 @@ import HomeView from '../views/HomeView.vue';
Vue.use(VueRouter)
const whiteSlideList = [ '/ui', '/hosInformation']; //侧边导航白名单
const blackHeaderList = ['/ErrorAccess']; //头部导航黑名单
const blackHeaderList = ['/ErrorAccess','/login']; //头部导航黑名单
const whiteFooterList = ['/','/doctorInformation' ,'/hosInformation','/addNewTreatment']; //底部白名单
const routes = [
{
path: '/login',
name: '登录',
component: () => import('../views/login.vue'),
meta: {
hideBreadcrumb: true, // 首页不显示面包屑
requiresAuth:true
}
},
{
path: '/',
name: '首页',
component: HomeView,
@ -18,6 +27,19 @@ const routes = [
}
},
{
path: '/hospitalManage',
name: '医院管理',
component: HomeView,
props:{
onlyHosFlag:true
},
meta: {
title: '医院管理',
hideBreadcrumb: true, // 首页不显示面包屑
requiresAuth:true
}
},
{
path: '/doctorInformation',
name: '医生信息',
component: () => import('../views/DoctorInformation.vue'),
@ -42,9 +64,6 @@ const routes = [
meta: {
title: '编辑医院',
breadcrumbParent: '医生信息' // 手动指定父级
// r如果想隐藏中间层级
// breadcrumbParent: '首页', // 跳过医生信息
// hideInBreadcrumb: true // 可选:隐藏当前项
}
},
{
@ -54,9 +73,6 @@ const routes = [
meta: {
title: '微信收款',
breadcrumbParent: '编辑医院' // 手动指定父级
// r如果想隐藏中间层级
// breadcrumbParent: '首页', // 跳过医生信息
// hideInBreadcrumb: true // 可选:隐藏当前项
}
},
@ -83,7 +99,8 @@ const routes = [
name: '收款方式',
meta: {
title: '收款管理',
hideBreadcrumb: true // 首页不显示面包屑
hideBreadcrumb: true, // 首页不显示面包屑
requiresAuth:true
}
},
{
@ -102,7 +119,22 @@ const router = new VueRouter({
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
// 检查本地存储是否有nick
const hasNick = localStorage.getItem('nick');
// 如果有nick且访问的是根路径,则重定向到/hospitalManage
if (hasNick && to.path === '/') {
next('/hospitalManage');
return;
}
// 如果没有nick且尝试访问需要认证的页面(如/hospitalManage),则重定向到首页
if (!hasNick && to.meta.requiresAuth) {
next('/');
return;
}
if (whiteSlideList.includes(to.path)) {
store.commit('SET_SIDEBAR', true); // 登录页面不显示侧边栏
} else {
@ -118,6 +150,7 @@ router.beforeEach((to, from, next) => {
} else {
store.commit('SET_HEADER', true); // 其他页面显示顶部
}
next();
});

12
src/utils/request.js

@ -15,7 +15,17 @@ const service = axios.create({
service.interceptors.request.use(
(config) => {
// 在发送请求之前做一些处理,例如添加 token
const token = localStorage.getItem("token");
// const token = localStorage.getItem("token");
const token = localStorage.getItem("authtoken");
// 检查是否存在 token,如果不存在则跳转到登录页
if (!token) {
// 可以根据需要决定是否提示用户
console.warn("未检测到登录信息,请重新登录");
window.location.href = "/login";
return Promise.reject(new Error("未登录"));
}
if (token) {
config.headers["Auth"] = `${token}`;
}

31
src/views/HomeView.vue

@ -1,7 +1,7 @@
<template>
<div class="min-width pagePadding " style="background: #F5F7FA;" v-if="pageShow">
<div class="doctor-list-wrap ">
<p class="pageTitle">医生列表</p>
<p class="pageTitle">{{onlyHosFlag ? '医院管理' :'医生列表'}}</p>
<el-form>
<div class="selectAllTable-wrap flex-between mt32">
<div class="left flex">
@ -16,16 +16,20 @@
@click="batchOperate('1')">
批量启用</GuipButton>
</div>
<div class="right flex">
<div class="right" v-if="onlyHosFlag">
<GuipButton type="system" size="form" @click="addHospitalUser">添加医院</GuipButton>
</div>
<div class="right flex" v-else>
<span>搜索医生</span>
<GuipInput ref="GuipInput" style="margin:0 24px 0 12px" width="280px" height="32px"
placeholder="输入姓名" @blur="inputBlur" v-model="doctorName" />
<GuipButton @click="addDoctor" size="form">新增医生</GuipButton>
</div>
</div>
<div class="tableList mt32" v-for="(item, index) in doctorList" :key="index">
<div class="top flex-between mb24">
<div class="top flex-between mb24" v-if="!onlyHosFlag">
<div class="left left1 flex">
<el-checkbox @change="handleCheckAllChange(index)" v-model="item.checked"></el-checkbox>
<el-avatar :src="item.avator" v-if="item.avator"></el-avatar>
@ -88,8 +92,7 @@
</template>
</el-table-column>
</GuipTable>
<PaymentMethod :list="payList[item.id] ? payList[item.id] : []" @updatePayStatus="updatePayStatus" :depart_id="item.id" :doctor_id="item.id"/>
<PaymentMethod :list="payList[item.id] ? payList[item.id] : []" @updatePayStatus="updatePayStatus" :depart_id="item.id" :doctor_id="item.id" v-if="!onlyHosFlag"/>
</div>
</el-form>
</div>
@ -107,6 +110,7 @@ import { mapState } from 'vuex';
import PaymentMethod from '@/views/paymentMethod.vue';
export default {
props:['onlyHosFlag'],
data() {
return {
list: {
@ -158,6 +162,7 @@ export default {
// CustomDropdown
},
created() {
console.log(this.onlyHosFlag,'onlyHosFlag===');
const { authtoken } = this.$route.query;
if(authtoken) {
localStorage.setItem('authtoken', authtoken);
@ -170,6 +175,10 @@ export default {
store.commit('SET_CUSTOMIZE', false);
store.commit('SET_SLIDER_MENU', 'menuData');
this.getInitData()
if(this.onlyHosFlag)return
// --
this.getBindpayList()
},
computed: {
...mapState(['menuData']) // VuexshowSidebar
@ -295,7 +304,7 @@ export default {
})
},
onSwitchChange1(row) {
console.log(row, '------flag');
// console.log(row, '------flag');
row.status = row.status == 0 ? 1 : 0;
this.$http('POST', '/api/admin/depart_mutil_option', {
depart_index_ids:row.id,
@ -315,7 +324,7 @@ export default {
this.$set(this.doctorList, row)
},
batchOperate(status) {
console.log(this.tableSelections,'this.tableSelections----');
// console.log(this.tableSelections,'this.tableSelections----');
if(!this.tableSelections || this.tableSelections.length <= 0) {
this.$message.error('未选择要'+(status == '0' ? '禁用':'启用')+'的科室')
return;
@ -393,6 +402,14 @@ export default {
query: { doctor_id: item.id }
})
},
addHospitalUser(){
// id token
let id = this.doctor_id;
this.$router.push({
name: '医院信息',
query: { doctor_id: id }
})
},
editDoctor(item) {
this.$router.push({
name: '医生信息',

382
src/views/login.vue

@ -0,0 +1,382 @@
<template>
<div class="index_loginPage__K_bGK login-wrap">
<div class="index_banner__1d8CU">
<div class="index_rootContainer">
<b class="title">扫码登录</b>
<p class="sub_title mt32">打开 <b>微信</b> 扫码登录</p>
<div class=" mb32" v-if="status == 'waiting'">
<vue-qr :text="qrCodeUrl" :size="192" :dot-scale="1"></vue-qr>
</div>
<div class="refreshCode" v-if="status == 'expired'" @click="refreshCode">
<i class="el-icon-refresh"></i>
刷新二维码
</div>
<!-- :logo-src="logoUrl"
:logo-scale="0.2" -->
<div class="attention gap10">
<el-checkbox v-model="checked"></el-checkbox>
<b>登录即代表同意 <a @click="jumpDoc">用户协议</a> <a @click="jumpDoc">隐私条款</a></b>
</div>
<p class="tip">若无账号请先联系客服注册</p>
</div>
</div>
<div class="style_footerWrapper">
<div class="left flex">
<img src="@/assets/doctor_h5.svg" alt="">
<div>
<b>手机端</b>
<p>微信扫码体验</p>
</div>
</div>
<div class="center">
<p><b>入驻指南</b>个人入驻 组织医院等入驻</p>
<p><b>人工咨询</b>扫左侧码进入手机端-我的-页面底部联系我们</p>
</div>
<div class="right">
<p><b>网站备案</b>© 2012-2025 something , <a href="">Inc. All rights 测试</a></p>
<p></p>
</div>
</div>
</div>
</template>
<script>
import VueQr from 'vue-qr'
export default {
components: {
VueQr
},
data() {
return {
checked: true,
qrCodeUrl: 'https://example.com',
token: '',
status: 'expired', // waiting, scanned, confirmed, expired, error
loginTime:'',
}
},
created(){
// this.generateQRCode()
},
beforeDestroy() {
this.clearTimers();
},
methods: {
jumpDoc() {
},
//
refreshCode() {
},
async generateQRCode() {
this.status = 'waiting';
this.qrCodeUrl = '';
try {
await this.$http('POST', '/api/admin/generate_qrcode', {
device_type: 'web',
timestamp: Date.now()
}).then(response => {
this.token = response.data.token;
this.qrCodeUrl = response.data.qr_code_url;
//
this.startPolling();
// 5
this.setExpireTimer();
}).catch(error => {
console.error('生成二维码失败:', error);
this.status = 'error';
});
} catch (error) {
console.error('生成二维码失败:', error);
this.status = 'error';
}
},
//
startPolling() {
this.clearTimers();
this.pollInterval = setInterval(async () => {
if (this.status === 'confirmed' || this.status === 'expired' || this.status === 'error') {
this.clearTimers();
return;
}
try {
await this.$http('POST', '/api/admin/check_scan_status', {
token: this.token
}).then(response => {
this.status = response.data.status;
if (response.data.status === 'scanned') {
//
setTimeout(() => {
this.handleLoginConfirm();
}, 2000);
} else if (response.data.status === 'confirmed') {
this.handleLoginSuccess(response.data);
} else if (response.data.status === 'expired') {
this.status = 'expired';
this.clearTimers();
}
}).catch(error => {
console.error('检查扫码状态失败:', error);
this.status = 'error';
this.clearTimers();
});
} catch (error) {
console.error('检查扫码状态失败:', error);
this.status = 'error';
this.clearTimers();
}
}, 2000);
},
//
async handleLoginConfirm() {
try {
await this.$http('POST', '/api/admin/confirm_login', {
token: this.token
}).then(response => {
if (response.data.success) {
this.handleLoginSuccess(response.data);
} else {
this.status = 'expired';
}
}).catch(error => {
console.error('登录确认失败:', error);
this.status = 'error';
});
} catch (error) {
console.error('登录确认失败:', error);
this.status = 'error';
}
},
//
handleLoginSuccess(data) {
this.status = 'confirmed';
this.userInfo = data.user_info;
this.loginTime = new Date().toLocaleString();
//
localStorage.setItem('authToken', data.auth_token);
localStorage.setItem('nick', JSON.stringify(data.nick));
localStorage.setItem('userInfo', JSON.stringify(data.user_info));
setTimeout(() => {
this.isLoggedIn = true;
this.$emit('login-success', data);
}, 1000);
},
//
setExpireTimer() {
this.expireTimer = setTimeout(() => {
if (this.status !== 'confirmed') {
this.status = 'expired';
this.clearTimers();
}
}, 5 * 60 * 1000); // 5
},
//
clearTimers() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
if (this.expireTimer) {
clearTimeout(this.expireTimer);
this.expireTimer = null;
}
},
}
}
</script>
<style lang="scss" scoped>
.index_banner__1d8CU {
align-items: center;
background-image: url(../assets/login_backImg.svg);
background-size: cover;
display: flex;
flex: 1 1;
justify-content: flex-end;
-webkit-transition: padding-right .5s;
transition: padding-right .5s;
width: 100%;
}
.index_rootContainer {
background: #fff;
border-radius: 16px;
box-shadow: 0 6px 40px 0 rgba(53, 76, 166, .08);
display: inline-block;
flex-shrink: 0;
height: 480px;
overflow: hidden;
width: 432px;
margin-right: 192px;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
.title {
font-family: Microsoft YaHei;
font-size: 22px;
font-weight: bold;
line-height: normal;
text-align: center;
letter-spacing: 0.08em;
color: #1E2226;
}
.sub_title {
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: normal;
line-height: normal;
letter-spacing: 0.08em;
color: #626573;
margin-top: 32px;
margin-bottom: 12px;
}
.img {
width: 192px;
height: 192px;
background: #006AFF;
margin: 0 auto;
margin-bottom: 32px;
}
}
.attention,
.tip {
font-size: 12px;
font-weight: normal;
line-height: 13px;
letter-spacing: 0.08em;
color: #626573;
text-align: center;
display: flex;
justify-content: center;
b {
font-size: 12px;
font-weight: normal;
a {
text-decoration: none;
color: #006AFF;
cursor: pointer;
}
}
}
.attention {
margin-bottom: 12px;
}
.refreshCode {
width: 172px;
height: 172px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
align-items: center;
color: #626573;
font-family: Arial, sans-serif;
font-weight: bold;
cursor: pointer;
margin: 20px auto;
i{
font-size: 28px;
}
}
@media(max-width: 1420px) {
.index_rootContainer {
margin-right: 102px;
}
}
.style_footerWrapper {
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
padding: 46px 49px;
width: 100%;
height: 192px;
box-sizing: border-box;
gap: 72px;
b {
font-size: 12px;
font-weight: bold;
line-height: 13px;
letter-spacing: 0.08em;
color: #1E2226;
}
.left {
gap: 12px;
div {
display: flex;
flex-direction: column;
gap: 8px;
text-align: left;
}
img {
width: 97px;
height: 100px;
}
}
.center,
.right {
display: flex;
flex-direction: column;
gap: 24px;
text-align: left;
p {
display: flex;
gap: 8px;
}
}
}
.style_iconWrapper__3qi7l {
flex-basis: 80px;
}
.index_loginPage__K_bGK {
display: flex;
flex-direction: column;
height: 100vh;
min-width: 1200px;
}
#ecomLoginForm {
height: 280px;
}
</style>

14
src/views/paymentMethod.vue

@ -9,7 +9,7 @@
</div>
</div>
<el-form>
<GuipTable :tableData="payList" :loading="tableLoading">
<GuipTable :tableData="payList" >
<el-table-column prop="type" fixed="left" label="类型" width="140">
<template slot-scope="scope">
<div class="flex gap10">
@ -108,12 +108,16 @@ export default {
list: {
deep: true,
handler(newVal) {
console.log(newVal,'newVal=====');
this.payList = [...newVal];
this.tableLoading = false;
}
}
},
doctor_id(newVal){
if(newVal) this.doctorId = newVal
},
depart_id(newVal){
if(newVal) this.departId = newVal
},
},
created(){
const {doctor_id,depart_id } = this.$route.query;
@ -121,8 +125,6 @@ export default {
this.departId =depart_id;
this.doctorId =doctor_id;
}
console.log(this.doctor_id,'=doctor_id===');
},
mounted() {
if(this.doctor_id){

Loading…
Cancel
Save