feat: 认证页面提价

This commit is contained in:
huxuejian 2026-01-23 14:36:28 +08:00
parent 9b545b442d
commit ae7b262840
35 changed files with 1106 additions and 473 deletions

View File

@ -1,3 +1,4 @@
MP_API_BASE_URL=http://192.168.60.2:8080 MP_API_BASE_URL=http://192.168.60.2:8080
MP_CACHE_PREFIX=development MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e MP_WX_APP_ID=wx93af55767423938e
MP_CORP_ID=wwe3fb2faa52cf9dfb

View File

@ -36,6 +36,9 @@ page {
.relative { .relative {
position: relative; position: relative;
} }
.absolute{
position: absolute;
}
.inline-block { .inline-block {
display: inline-block; display: inline-block;

View File

@ -1,5 +1,5 @@
<template> <template>
<view v-if="showCancel || showConfirm" class="relative px-15 py-12 bg-white text-center" <view v-if="showCancel || showConfirm" class="relative flex px-15 py-12 bg-white text-center"
:class="hidedenShadow ? '' : 'shadow-up'"> :class="hidedenShadow ? '' : 'shadow-up'">
<view v-if="showCancel" class="flex-grow py-10 text-base border-primary rounded text-primary rounded" <view v-if="showCancel" class="flex-grow py-10 text-base border-primary rounded text-primary rounded"
@click="cancel()"> @click="cancel()">

View File

@ -1,5 +1,5 @@
<template> <template>
<picker mode="selector" :range="displayRange" :disabled="disableChange" @change="change($event)"> <picker mode="selector" :range="displayRange" :disabled="disableChange" @change="change($event)">
<common-cell :name="name" :required="required"> <common-cell :name="name" :required="required">
<view class="form-content__wrapper"> <view class="form-content__wrapper">
<view class="flex-main-content truncate" :class="value ? '' : 'form__placeholder'"> <view class="flex-main-content truncate" :class="value ? '' : 'form__placeholder'">
@ -49,9 +49,9 @@ const displayRange = computed(() => {
} }
return props.range; return props.range;
}) })
const value = computed(() => { const value = computed(() => {
if (!props.form || !props.form[props.title]) return ''; if (!props.form || !props.form[props.title]) return '';
const currentValue = props.form[props.title]; const currentValue = props.form[props.title];
// rangelabel // rangelabel
if (Array.isArray(props.range) && props.range.length > 0 && typeof props.range[0] === 'object') { if (Array.isArray(props.range) && props.range.length > 0 && typeof props.range[0] === 'object') {
@ -65,7 +65,7 @@ function change(e) {
const selectedValue = props.range[e.detail.value]; const selectedValue = props.range[e.detail.value];
emits('change', { emits('change', {
title: props.title, title: props.title,
value: selectedValue value: typeof selectedValue === 'object' ? selectedValue.value : selectedValue
}) })
} }
</script> </script>

View File

@ -4,8 +4,9 @@
{{ name }}<text v-if="required" class="form-cell--required"></text> {{ name }}<text v-if="required" class="form-cell--required"></text>
</view> </view>
<view class="mt-10"> <view class="mt-10">
<textarea :disabled="disableChange" :value="value" class="form-textarea" :placeholder="placeholder" <textarea :disabled="disableChange" :value="value" class="form-textarea"
placeholder-class="form__placeholder" :maxlength="wordLimit" @input="change($event)" /> :class="border ? 'form-textarea--border' : ''" :placeholder="placeholder" placeholder-class="form__placeholder"
:maxlength="wordLimit" @input="change($event)" />
<view v-if="wordLimit > 0" class="form-textarea__count"> <view v-if="wordLimit > 0" class="form-textarea__count">
{{ value && value.length ? value.length : 0 }} / {{ wordLimit }} {{ value && value.length ? value.length : 0 }} / {{ wordLimit }}
</view> </view>
@ -17,6 +18,10 @@ import { computed } from 'vue';
const emits = defineEmits(['change']); const emits = defineEmits(['change']);
const props = defineProps({ const props = defineProps({
border: {
type: Boolean,
default: true
},
form: { form: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
@ -45,7 +50,7 @@ const placeholder = computed(() => `请输入${props.name || ''}`)
const value = computed(() => props.form && props.form && props.form[props.title] ? props.form[props.title] : '') const value = computed(() => props.form && props.form && props.form[props.title] ? props.form[props.title] : '')
const wordLimit = computed(() => { const wordLimit = computed(() => {
if (typeof props.wordLimit === 'string' && Number(props.wordLimit) > 0) { if (typeof props.wordLimit === 'string' && Number(props.wordLimit) > 0) {
return Number.ceil(props.wordLimit) return Math.ceil(props.wordLimit)
} }
if (typeof props.wordLimit === 'number' && props.wordLimit > 0) { if (typeof props.wordLimit === 'number' && props.wordLimit > 0) {
return props.wordLimit return props.wordLimit
@ -72,12 +77,15 @@ function change(e) {
.form-textarea { .form-textarea {
width: 100%; width: 100%;
font-size: 28rpx; font-size: 28rpx;
border: 1px solid #eee;
padding: 20rpx; padding: 20rpx;
border-radius: 8rpx; border-radius: 8rpx;
box-sizing: border-box; box-sizing: border-box;
} }
.form-textarea--border {
border: 1px solid #eee;
}
.form-textarea__count { .form-textarea__count {
padding-top: 20rpx; padding-top: 20rpx;
text-align: right; text-align: right;

View File

@ -1,9 +1,9 @@
<template> <template>
<view class="full-page" :style="pageStyle"> <view class="full-page" :class="pageClass" :style="pageStyle">
<view v-if="hasHeader" class="page-header"> <view v-if="hasHeader" class="page-header">
<slot name="header"></slot> <slot name="header"></slot>
</view> </view>
<view class="page-main" :style="mainStyle"> <view class="page-main" :class="mainClass" :style="mainStyle">
<view v-if="customScroll" class="page-scroll"> <view v-if="customScroll" class="page-scroll">
<slot></slot> <slot></slot>
</view> </view>
@ -16,7 +16,7 @@
<slot name="footer"></slot> <slot name="footer"></slot>
</view> </view>
<!-- #ifdef MP--> <!-- #ifdef MP-->
<view class="safeareaBottom"></view> <view v-if="showSafeArea" class="safeareaBottom"></view>
<!-- #endif --> <!-- #endif -->
</view> </view>
</template> </template>
@ -27,8 +27,11 @@ import useDebounce from '@/utils/useDebounce';
const emits = defineEmits(['reachBottom']); const emits = defineEmits(['reachBottom']);
const props = defineProps({ const props = defineProps({
customScroll: { type: Boolean, default: false }, customScroll: { type: Boolean, default: false },
mainClass: { type: String, default: '' },
mainStyle: { default: '' }, mainStyle: { default: '' },
pageStyle: { default: '' } pageClass: { type: String, default: '' },
pageStyle: { default: '' },
showSafeArea: { type: Boolean, default: true }
}); });
const slots = useSlots(); const slots = useSlots();
const hasHeader = computed(() => !!slots.header); const hasHeader = computed(() => !!slots.header);

View File

@ -39,7 +39,9 @@ export default function useGuard() {
async function triggleShowEvents() { async function triggleShowEvents() {
await promise; await promise;
onShowEvents.value.forEach(fn => fn(onShowOptions.value)) if (account.value && account.value.openid) {
onShowEvents.value.forEach(fn => fn(onShowOptions.value))
}
} }
function useShow(fn) { function useShow(fn) {
@ -53,8 +55,10 @@ export default function useGuard() {
const route = routes.find(i => page && i.path === page.route); const route = routes.find(i => page && i.path === page.route);
const requireLogin = route && route.meta && route.meta.login; const requireLogin = route && route.meta && route.meta.login;
if (requireLogin && !account.value) { if (requireLogin && !account.value) {
const res = await login() await login()
if (res) { console.log('login success')
console.log(account.value)
if (account.value) {
resolve() resolve()
} else { } else {
return toLoginPage(opts, page.route); return toLoginPage(opts, page.route);

View File

@ -3,13 +3,8 @@
{ {
"path": "pages/message/message", "path": "pages/message/message",
"style": { "style": {
"navigationBarTitleText": "消息" "navigationBarTitleText": "消息",
} "navigationStyle": "custom"
},
{
"path": "pages/case/case",
"style": {
"navigationBarTitleText": "病例"
} }
}, },
{ {
@ -18,6 +13,12 @@
"navigationBarTitleText": "工作台" "navigationBarTitleText": "工作台"
} }
}, },
{
"path": "pages/case/case",
"style": {
"navigationBarTitleText": "病例"
}
},
{ {
"path": "pages/work/profile", "path": "pages/work/profile",
"style": { "style": {
@ -30,10 +31,22 @@
"navigationBarTitleText": "选择科室" "navigationBarTitleText": "选择科室"
} }
}, },
{
"path": "pages/work/verify/assistant",
"style": {
"navigationBarTitleText": "上传证照"
}
},
{
"path": "pages/work/verify/doctor",
"style": {
"navigationBarTitleText": "上传证照"
}
},
{ {
"path": "pages/login/login", "path": "pages/login/login",
"style": { "style": {
"navigationBarTitleText": "登录" "navigationBarTitleText": "授权登录"
} }
} }
], ],

View File

@ -1,5 +1,5 @@
<template> <template>
<view v-if="team" class="pt-lg px-15 flex flex-col items-center text-center"> <!-- <view v-if="team" class="pt-lg px-15 flex flex-col items-center text-center">
<group-avatar :avatarList="team.avatars" /> <group-avatar :avatarList="team.avatars" />
<view class="mt-15 text-base font-semibold text-dark">{{ <view class="mt-15 text-base font-semibold text-dark">{{
team.teamName team.teamName
@ -8,20 +8,15 @@
<view class="mt-15 text-lg text-dark font-semibold" <view class="mt-15 text-lg text-dark font-semibold"
>为您提供团队个性化专属服务</view >为您提供团队个性化专属服务</view
> >
</view> v-else
<view v-else class="pt-lg px-15 flex flex-col items-center text-center"> </view> -->
<view class="pt-lg px-15 flex flex-col items-center text-center">
<image src="/static/logo-plain.png" class="logo"></image> <image src="/static/logo-plain.png" class="logo"></image>
<view class="mt-15 text-xl font-semibold text-dark">柚健康</view> <view class="mt-15 text-xl font-semibold text-dark">柚健康</view>
<view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view> <view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view>
</view> </view>
<view class="login-btn-wrap"> <view class="login-btn-wrap">
<button <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
v-if="checked"
class="login-btn"
type="primary"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
手机号快捷登录 手机号快捷登录
</button> </button>
<!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()"> <!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
@ -31,10 +26,7 @@
手机号快捷登录 手机号快捷登录
</button> </button>
</view> </view>
<view <view class="flex items-center justify-center mt-12 px-15" @click="checked = !checked">
class="flex items-center justify-center mt-12 px-15"
@click="checked = !checked"
>
<checkbox :checked="checked" style="transform: scale(0.7)" /> <checkbox :checked="checked" style="transform: scale(0.7)" />
<view class="text-sm text-gray">我已阅读并同意</view> <view class="text-sm text-gray">我已阅读并同意</view>
<view class="text-sm text-primary">用户协议</view> <view class="text-sm text-primary">用户协议</view>
@ -44,16 +36,16 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { storeToRefs } from "pinia";
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { get } from "@/utils/cache"; import { get } from "@/utils/cache";
import { toast } from "@/utils/widget"; import { toast } from "@/utils/widget";
import groupAvatar from "@/components/group-avatar.vue";
const team = ref(true); const team = ref(true);
const checked = ref(false); const checked = ref(false);
const redirectUrl = ref(""); const redirectUrl = ref("");
const { doctorInfo } = storeToRefs(useAccountStore());
const { login } = useAccountStore(); const { login } = useAccountStore();
function attempRedirect(url) { function attempRedirect(url) {
@ -81,8 +73,8 @@ function remind() {
} }
function toHome() { function toHome() {
uni.navigateTo({ uni.switchTab({
url: "/pages/message/message", url: "/pages/work/work",
}); });
} }
@ -92,6 +84,10 @@ async function getPhoneNumber(e) {
const res = await login(phoneCode); const res = await login(phoneCode);
if (res && redirectUrl.value) { if (res && redirectUrl.value) {
await attempToPage(redirectUrl.value); await attempToPage(redirectUrl.value);
} else if (res && !doctorInfo.value) {
uni.redirectTo({
url: '/pages/work/profile'
})
} else if (res) { } else if (res) {
toHome(); toHome();
} }
@ -109,7 +105,6 @@ onLoad((opts) => {
if (opts.source === "teamInvite") { if (opts.source === "teamInvite") {
team.value = get("invite-team-info"); team.value = get("invite-team-info");
redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`; redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
console.log("redirectUrl", redirectUrl.value);
return; return;
} }
if (opts.redirect) { if (opts.redirect) {

View File

@ -0,0 +1,64 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="bg-white rounded overflow-hidden" style="width: 690rpx;">
<view class="px-15 py-12 text-center text-lg font-semibold text-dark">
认证须知
</view>
<view class="text-base text-dark px-15 leading-normal font-semibold mt-10">
1认证通过后您个人账号病历档案管理数含所有团队上限由10个升级至100个
</view>
<view class="px-15 leading-normal mt-10 ">
<text class="text-base text-dark">2认证前请仔细核对个人信息确保准确无误认证后部分信息不支持修改包括姓名岗位等如需修改以上信息请联系客服人工处理</text>
</view>
<view class="mt-10 px-15 leading-normal font-semibold pb-50 text-lg text-primary" @click="toService()">点击添加客服
</view>
<view class="footer-buttons">
<button-footer hideden-shadow confirmText="去认证" @confirm="confirm()" @cancel="close()" />
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, watch } from 'vue';
import ButtonFooter from '@/components/button-footer.vue';
const emits = defineEmits(['close', 'confirm'])
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const popup = ref()
function close() {
emits('close')
}
function confirm() {
close()
uni.navigateTo({
url: "/pages/work/profile?type=cert",
});
}
function toService() {
close()
}
watch(() => props.visible, n => {
if (n) {
popup.value && popup.value.open()
} else {
popup.value && popup.value.close()
}
})
</script>
<style lang="scss" scoped>
.pb-50 {
padding-bottom: 100rpx;
}
</style>

308
pages/work/profile copy.vue Normal file
View File

@ -0,0 +1,308 @@
<template>
<view class="profile-page">
<!-- 表单区域 -->
<view class="form-section bg-white">
<!-- 姓名 -->
<form-input
name="姓名"
:required="true"
:form="formData"
title="anotherName"
@change="handleFieldChange"
/>
<!-- 头像 -->
<common-cell name="头像">
<view class="form-content__wrapper" @click="chooseAvatar">
<view class="flex-main-content flex items-center">
<image
v-if="formData.avatar"
class="avatar-preview"
:src="formData.avatar"
mode="aspectFill"
/>
</view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view>
</common-cell>
<!-- 性别 -->
<form-select
name="性别"
:form="formData"
title="gender"
:range="genderOptions"
@change="handleFieldChange"
/>
<!-- 手机号不可修改 -->
<common-cell name="手机号 (不可修改)">
<view class="form-content__wrapper">
<view class="flex-main-content text-dark">{{ formData.mobile }}</view>
</view>
</common-cell>
<!-- 岗位 -->
<form-select
name="岗位"
:form="formData"
title="position"
:range="positionOptions"
@change="handleFieldChange"
/>
<!-- 职称 -->
<form-select
name="职称"
:form="formData"
title="title"
:range="titleOptions"
@change="handleFieldChange"
/>
<!-- 科室 -->
<common-cell name="科室">
<view class="form-content__wrapper" @click="openDepartmentSelect">
<view
class="flex-main-content text-right"
:class="{ 'text-placeholder': !formData.departmentName }"
>
{{ formData.departmentName || "请选择" }}
</view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view>
</common-cell>
<!-- 个人介绍 -->
<form-textarea
name="个人介绍"
:form="formData"
title="intro"
:word-limit="500"
@change="handleFieldChange"
/>
</view>
<!-- 底部按钮 -->
<view class="button-footer">
<view class="btn btn-cancel" @click="handleCancel">取消</view>
<view class="btn btn-save" @click="handleSave">保存</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import useGuard from "@/hooks/useGuard.js";
import { chooseAndUploadImage } from "@/utils/file.js";
import useAccountStore from "@/store/account.js";
import { storeToRefs } from "pinia";
import CommonCell from "@/components/form-template/common-cell.vue";
import FormInput from "@/components/form-template/form-cell/form-input.vue";
import FormSelect from "@/components/form-template/form-cell/form-select.vue";
import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue";
import api from "@/utils/api.js";
const { account, doctorInfo } = storeToRefs(useAccountStore());
const { useLoad } = useGuard();
const { getDoctorInfo } = useAccountStore();
//
const formData = ref({
anotherName: "",
avatar: "",
gender: "",
mobile: "",
position: "",
title: "",
department: "",
departmentName: "",
departmentId: "",
intro: "",
});
//
const genderOptions = [
{ label: "男", value: "0" },
{ label: "女", value: "1" },
];
const positionOptions = ["医生", "护士", "药师", "技师", "其他"];
const titleOptions = ["主任医师", "副主任医师", "主治医师", "医师", "其他"];
//
const handleFieldChange = (e) => {
if (e.title === "gender") {
formData.value[e.title] = e.value.value;
} else {
formData.value[e.title] = e.value;
}
};
//
const chooseAvatar = async () => {
const url = await chooseAndUploadImage();
if (url) {
formData.value.avatar = url;
}
};
const handleCancel = () => {
uni.navigateBack();
};
//
const handleSave = async () => {
createDoctorInfo();
};
useLoad(() => {
if (doctorInfo.value) {
formData.value = { ...doctorInfo.value };
} else {
formData.value.mobile = account.value.mobile;
}
});
//
const createDoctorInfo = async () => {
if (!formData.value.anotherName) {
uni.showToast({
title: "请输入姓名",
icon: "none",
});
return;
}
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
mobile: formData.value.mobile,
weChatOpenId: account.value.openid,
deptIds: [],
loginTypes: ["wxApp"],
corpId: account.value.corpId,
};
const res = await api("addCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "创建成功",
icon: "success",
});
await getDoctorInfo();
uni.navigateBack();
} else {
uni.showToast({
title: "创建失败",
icon: "none",
});
console.error("创建医生信息失败:", res);
}
};
const updateDoctorInfo = async () => {
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
};
const res = await api("updateCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "更新成功",
icon: "success",
});
}
await getDoctorInfo();
uni.navigateBack();
};
//
const openDepartmentSelect = () => {
uni.navigateTo({
url: "/pages/work/department-select",
events: {
deptSelected: ({ name, deptId }) => {
formData.value.department = name || "";
formData.value.departmentName = name || "";
formData.value.departmentId = deptId || "";
},
},
});
};
</script>
<style lang="scss" scoped>
.profile-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.form-section {
margin-top: 20rpx;
}
.form-content__wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
text-align: right;
font-size: 28rpx;
input {
text-align: right;
}
}
.text-placeholder {
color: #999;
}
.avatar-preview {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
}
.avatar-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
.avatar-icon {
font-size: 48rpx;
color: #999;
}
}
.button-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 20rpx 30rpx;
background: #fff;
border-top: 1px solid #eee;
gap: 20rpx;
z-index: 100;
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 8rpx;
font-size: 32rpx;
}
.btn-cancel {
background: #fff;
border: 1px solid #007aff;
color: #007aff;
}
.btn-save {
background: #007aff;
color: #fff;
}
}
</style>

View File

@ -1,211 +1,83 @@
<template> <template>
<view class="profile-page"> <full-page>
<!-- 表单区域 --> <view class="p-15">
<view class="form-section bg-white"> <view class="bg-white px-10 mb-10 rounded">
<!-- 姓名 --> <form-input :form="formData" required wordLimit="10" title="anotherName" name="姓名" @change="onChange($event)" />
<form-input <common-cell title="avatar" name="头像">
name="姓名" <view class="flex-grow flex items-center justify-end" @click="chooseAvatar()">
:required="true" <image v-if="formData.avatar" class="avatar mr-5 rounded-full" :src="formData.avatar" />
:form="formData" <image v-else class="avatar mr-5 rounded-full" src="/static/default-avatar.png" />
title="anotherName" <uni-icons color="#999" type="right" size="16" />
@change="handleFieldChange"
/>
<!-- 头像 -->
<common-cell name="头像">
<view class="form-content__wrapper" @click="chooseAvatar">
<view class="flex-main-content flex items-center">
<image
v-if="formData.avatar"
class="avatar-preview"
:src="formData.avatar"
mode="aspectFill"
/>
</view> </view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons> </common-cell>
</view> <form-select :form="formData" name="性别" title="gender" :range="genderOptions" @change="onChange($event)" />
</common-cell> <form-input :form="formData" disableChange wordLimit="11" title="mobile" name="手机号 (不可修改)" />
<!-- 性别 --> </view>
<form-select
name="性别"
:form="formData"
title="gender"
:range="genderOptions"
@change="handleFieldChange"
/>
<!-- 手机号不可修改 -->
<common-cell name="手机号 (不可修改)">
<view class="form-content__wrapper">
<view class="flex-main-content text-dark">{{ formData.mobile }}</view>
</view>
</common-cell>
<!-- 岗位 -->
<form-select
name="岗位"
:form="formData"
title="position"
:range="positionOptions"
@change="handleFieldChange"
/>
<!-- 职称 -->
<form-select
name="职称"
:form="formData"
title="title"
:range="titleOptions"
@change="handleFieldChange"
/>
<!-- 科室 -->
<common-cell name="科室">
<view class="form-content__wrapper" @click="openDepartmentSelect">
<view
class="flex-main-content text-right"
:class="{ 'text-placeholder': !formData.departmentName }"
>
{{ formData.departmentName || "请选择" }}
</view>
<uni-icons class="form-arrow" type="arrowright"></uni-icons>
</view>
</common-cell>
<!-- 个人介绍 --> <view class="bg-white px-10 mb-10 rounded">
<form-textarea <common-cell title="avatar" name="岗位">
name="个人介绍" <view class="flex-grow flex items-center justify-end">
:form="formData" <!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
title="intro" <uni-icons color="#999" type="right" size="16" />
:word-limit="500" </view>
@change="handleFieldChange" </common-cell>
/> <common-cell title="avatar" name="职称">
<view class="flex-grow flex items-center justify-end">
<!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
<uni-icons color="#999" type="right" size="16" />
</view>
</common-cell>
<common-cell title="avatar" name="科室">
<view class="flex-grow flex items-center justify-end">
<!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
<uni-icons color="#999" type="right" size="16" />
</view>
</common-cell>
</view>
<view class="bg-white rounded">
<form-textarea :border="false" :form="formData" title="intro" name="个人介绍" :wordLimit="300"
@change="onChange($event)" />
</view>
</view> </view>
<!-- 底部按钮 --> <template #footer>
<view class="button-footer"> <button-footer :cancelText="cancelText" :confirmText="confirmText" @confirm="save()" @cancel="back()" />
<view class="btn btn-cancel" @click="handleCancel">取消</view> </template>
<view class="btn btn-save" @click="handleSave">保存</view> </full-page>
</view>
</view>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref } from "vue";
import useGuard from "@/hooks/useGuard.js";
import { chooseAndUploadImage } from "@/utils/file.js";
import useAccountStore from "@/store/account.js";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import CommonCell from "@/components/form-template/common-cell.vue"; import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import { upload } from "@/utils/http.js";
import { toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import commonCell from "@/components/form-template/common-cell.vue";
import FormInput from "@/components/form-template/form-cell/form-input.vue"; import FormInput from "@/components/form-template/form-cell/form-input.vue";
import FormSelect from "@/components/form-template/form-cell/form-select.vue"; import FormSelect from "@/components/form-template/form-cell/form-select.vue";
import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue"; import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue";
import api from "@/utils/api.js"; import fullPage from '@/components/full-page.vue';
const { account, doctorInfo } = storeToRefs(useAccountStore()); const { account, doctorInfo } = storeToRefs(useAccountStore());
const { useLoad } = useGuard(); const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore(); const { getDoctorInfo } = useAccountStore();
//
const formData = ref({ const form = ref({});
anotherName: "", const type = ref('')
avatar: "", const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value, mobile: account.value?.mobile }));
gender: "", const cancelText = computed(() => doctorInfo.value ? '取消' : '暂不填写');
mobile: "", const confirmText = computed(() => type.value === 'cert' ? '下一步' : '保存');
position: "",
title: "",
department: "",
departmentName: "",
departmentId: "",
intro: "",
});
// //
const genderOptions = [ const genderOptions = [
{ label: "男", value: "0" }, { label: "男", value: "0" },
{ label: "女", value: "1" }, { label: "女", value: "1" },
]; ];
const positionOptions = ["医生", "护士", "药师", "技师", "其他"];
const titleOptions = ["主任医师", "副主任医师", "主治医师", "医师", "其他"];
//
const handleFieldChange = (e) => {
if (e.title === "gender") {
formData.value[e.title] = e.value.value;
} else {
formData.value[e.title] = e.value;
}
};
//
const chooseAvatar = async () => {
const url = await chooseAndUploadImage();
if (url) {
formData.value.avatar = url;
}
};
const handleCancel = () => {
uni.navigateBack();
};
//
const handleSave = async () => {
createDoctorInfo();
};
useLoad(() => {
if (doctorInfo.value) {
formData.value = { ...doctorInfo.value };
} else {
formData.value.mobile = account.value.mobile;
}
});
//
const createDoctorInfo = async () => {
if (!formData.value.anotherName) {
uni.showToast({
title: "请输入姓名",
icon: "none",
});
return;
}
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
mobile: formData.value.mobile,
weChatOpenId: account.value.openid,
deptIds: [],
loginTypes: ["wxApp"],
corpId: account.value.corpId,
};
const res = await api("addCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "创建成功",
icon: "success",
});
await getDoctorInfo();
uni.navigateBack();
} else {
uni.showToast({
title: "创建失败",
icon: "none",
});
console.error("创建医生信息失败:", res);
}
};
const updateDoctorInfo = async () => {
let params = {
anotherName: formData.value.anotherName,
avatar: formData.value.avatar,
gender: formData.value.gender,
};
const res = await api("updateCorpMember", {
params,
});
if (res.success && res.data) {
uni.showToast({
title: "更新成功",
icon: "success",
});
}
await getDoctorInfo();
uni.navigateBack();
};
// //
const openDepartmentSelect = () => { const openDepartmentSelect = () => {
uni.navigateTo({ uni.navigateTo({
@ -219,89 +91,74 @@ const openDepartmentSelect = () => {
}, },
}); });
}; };
function back() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url: "/pages/work/work",
});
}
}
function chooseAvatar() {
uni.chooseImage({
count: 1,
success: async (res) => {
const [path] = res.tempFilePaths;
const url = await upload(path);
if (url) {
form.value.avatar = url;
} else {
toast('上传失败')
}
}
})
}
function onChange({ title, value }) {
form.value[title] = value
}
async function save() {
if (typeof formData.value.anotherName !== 'string' || !formData.value.anotherName.trim()) {
return toast('请输入姓名')
}
if (type.value === 'cert' && !formData.value.departmentId) {
return toast('请输入岗位信息')
}
const apiName = doctorInfo.value ? 'updateCorpMemberFromWxapp' : 'addCorpMemberFromWxapp';
const data = {
...form.value,
weChatOpenId: account.value.openid,
mobile: account.value.mobile,
corpId: account.value.corpId,
}
const res = await api(apiName, data);
if (res && res.success) {
await toast('保存成功');
await getDoctorInfo()
back()
} else {
await toast(res?.message || '保存失败');
}
}
useLoad(opts => {
type.value = opts?.type;
})
useShow(() => {
getDoctorInfo()
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.profile-page { .avatar {
min-height: 100vh; width: 64rpx;
background: #f5f5f5; height: 64rpx;
padding-bottom: 120rpx;
}
.form-section {
margin-top: 20rpx;
}
.form-content__wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
text-align: right;
font-size: 28rpx;
input {
text-align: right;
}
}
.text-placeholder {
color: #999;
}
.avatar-preview {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
}
.avatar-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
.avatar-icon {
font-size: 48rpx;
color: #999;
}
}
.button-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 20rpx 30rpx;
background: #fff;
border-top: 1px solid #eee;
gap: 20rpx;
z-index: 100;
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 8rpx;
font-size: 32rpx;
}
.btn-cancel {
background: #fff;
border: 1px solid #007aff;
color: #007aff;
}
.btn-save {
background: #007aff;
color: #fff;
}
} }
</style> </style>

View File

@ -0,0 +1,164 @@
<template>
<full-page pageClass="bg-white">
<view class="p-15">
<view class="mt-15 title-bar relative font-semibold">
<text class="mr-5 text-dark text-base">请上传</text>
<text class="text-primary text-base">身份证正反面</text>
</view>
<view class="mt-12 flex items-center justify-between">
<view class="album border rounded overflow-hidden" @click="uploadIdCard('idCardFront')">
<image v-if="form.idCardFront" class="w-full h-full" :src="form.idCardFront" />
<view v-else class="relative w-full h-full ">
<image class="absolute w-full h-full" src="/static/work/fIDCard.png" />
<view class="absolute w-full h-full flex flex-col items-center justify-center">
<image class="carmra-icon" src="/static/work/camera.png" />
<view class="text-lg font-semibold color-428">上传人像面</view>
</view>
</view>
</view>
<view class="album border rounded overflow-hidden" @click="uploadIdCard('idCardBack')">
<image v-if="form.idCardBack" class="w-full h-full" :src="form.idCardBack" />
<view v-else class="relative w-full h-full ">
<image class="absolute w-full h-full" src="/static/work/aIDCard.png" />
<view class="absolute w-full h-full flex flex-col items-center justify-center">
<image class="carmra-icon" src="/static/work/camera.png" />
<view class="text-lg font-semibold color-428">上传国徽面</view>
</view>
</view>
</view>
</view>
<view class="exam px-15 rounded-sm leading-normal text-dark text-base">
身份证示例
</view>
<view class="mt-12 flex items-center justify-between">
<image class="album" src="/static/work/cardFront.png" />
<image class="album" src="/static/work/cardBack.png" />
</view>
<view class="mt-12 text-dark font-semibold text-base">拍摄须知</view>
<view class="mt-10 flex items-center justify-between">
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/fIDCard.png" />
<image class="mb-5 status-icon" src="/static/work/hook.png" />
<view class="text-base text-dark">标准</view>
</view>
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/lackCard.png" />
<image class="mb-5 status-icon" src="/static/work/error.png" />
<view class="text-base text-dark">缺边</view>
</view>
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/vagueCard.png" />
<image class="mb-5 status-icon" src="/static/work/error.png" />
<view class="text-base text-dark">模糊</view>
</view>
<view class="flex flex-col items-center justify-center">
<image class="mb-5 exam-icon" src="/static/work/flashCard.png" />
<image class="mb-5 status-icon" src="/static/work/error.png" />
<view class="text-base text-dark">闪光</view>
</view>
</view>
</view>
<template #footer>
<button-footer cancelText="上一步" confirmText="提交审核" @confirm="save()" @cancel="back()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import { upload } from "@/utils/http.js";
import { hideLoading, loading as showLoading, toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import fullPage from '@/components/full-page.vue';
useGuard();
const { account, doctorInfo } = storeToRefs(useAccountStore());
const form = ref({})
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
function back() {
uni.navigateBack()
}
function uploadIdCard(type) {
uni.chooseImage({
count: 1,
success: async (res) => {
const [path] = res.tempFilePaths;
showLoading('正在上传')
const url = await upload(path);
hideLoading()
if (url) {
form.value[type] = url;
} else {
toast('上传失败')
}
}
})
}
async function save() {
if (typeof formData.value.idCardFront !== 'string' || formData.value.idCardFront.trim() === '') {
return toast('请上传人像面')
}
if (typeof formData.value.idCardBack !== 'string' || formData.value.idCardBack.trim() === '') {
return toast('请上传国徽面')
}
console.log('form.value: ', formData.value)
// uni.showToast({ title: '', icon: 'none' })
}
</script>
<style scoped>
.title-bar {
padding-left: 20rpx;
}
.title-bar::before {
content: "";
position: absolute;
left: 0;
top: 50%;
height: 80%;
width: 8rpx;
transform: translateY(-50%);
border-radius: 4rpx;
background: #0074ff;
}
.album {
width: 330rpx;
height: 200rpx;
}
.carmra-icon {
width: 96rpx;
height: 96rpx;
}
.color-428 {
color: #428bf0;
}
.exam {
margin-top: 80rpx;
background: #dde6f6;
padding: 8rpx 30rpx;
width: fit-content;
}
.exam-icon {
width: 160rpx;
height: 104rpx;
}
.status-icon {
width: 40rpx;
height: 40rpx;
}
</style>

View File

@ -0,0 +1,126 @@
<template>
<full-page pageClass="bg-white">
<view class="p-15">
<view class="title-bar relative text-dark text-base font-semibold">请填写执业医院</view>
<view class="mt-12 p-10 flex items-center justify-between border rounded">
<view class="w-0 flex-grow truncate text-base mr-10" :class="formData.hospitalName ? 'text-dark' : 'text-gray'">
{{ formData.hospitalName || '请填写执业医院' }}
</view>
<uni-icons class="flex-shrink-0" color="#666" type="right" size="16" />
</view>
<view class="mt-15 title-bar relative font-semibold">
<text class="mr-5 text-dark text-base">请上传</text>
<text class="text-primary text-base">医师执业资格证</text>
</view>
<view class="mt-12 flex items-center justify-between">
<view class="album border rounded overflow-hidden" @click="uploadLicense('medicalLicenseFront')">
<image v-if="formData.medicalLicenseFront" class="w-full h-full" :src="formData.medicalLicenseFront" />
<view v-else class="w-full h-full flex flex-col items-center justify-center">
<uni-icons color="#666" type="camera" size="36" />
<view class="text-dark text-base">上传第一页</view>
</view>
</view>
<view class="album border rounded overflow-hidden" @click="uploadLicense('medicalLicenseBack')">
<image v-if="formData.medicalLicenseBack" class="w-full h-full" :src="formData.medicalLicenseBack" />
<view v-else class="w-full h-full flex flex-col items-center justify-center">
<uni-icons color="#666" type="camera" size="36" />
<view class="text-dark text-base">上传第二页</view>
</view>
</view>
</view>
<view class="exam px-15 rounded-sm leading-normal text-dark text-base">
证书示例
</view>
<view class="mt-10 text-sm text-dark leading-normal">1确保姓名照片编号执业范围签发机关等清晰可见</view>
<view class="mt-10 text-sm text-dark leading-normal">2需上传证书第一第二页图片仅供参考以实际证书为准</view>
<view class="mt-12 flex items-center justify-between">
<image class="album" src="/static/work/licenseFront.png" />
<image class="album" src="/static/work/licenseBack.png" />
</view>
</view>
<template #footer>
<button-footer cancelText="上一步" confirmText="提交审核" @confirm="save()" @cancel="back()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import { upload } from "@/utils/http.js";
import { hideLoading, loading as showLoading, toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import fullPage from '@/components/full-page.vue';
useGuard();
const { account, doctorInfo } = storeToRefs(useAccountStore());
const form = ref({});
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value }))
function back() {
uni.navigateBack()
}
function uploadLicense(type) {
uni.chooseImage({
count: 1,
success: async (res) => {
const [path] = res.tempFilePaths;
showLoading('正在上传')
const url = await upload(path);
hideLoading()
if (url) {
form.value[type] = url;
} else {
toast('上传失败')
}
}
})
}
async function save() {
if (typeof formData.value.hospitalName !== 'string' || formData.value.hospitalName.trim() === '') {
return toast('请填写执业医院')
}
if (typeof formData.value.medicalLicenseFront !== 'string' || formData.value.medicalLicenseFront.trim() === '') {
return toast('请上传医师执业资格证第一页')
}
if (typeof formData.value.medicalLicenseBack !== 'string' || formData.value.medicalLicenseBack.trim() === '') {
return toast('请上传医师执业资格证第二页')
}
console.log('form.value: ', formData.value)
// uni.showToast({ title: '', icon: 'none' })
}
</script>
<style scoped>
.title-bar {
padding-left: 20rpx;
}
.title-bar::before {
content: "";
position: absolute;
left: 0;
top: 50%;
height: 80%;
width: 8rpx;
transform: translateY(-50%);
border-radius: 4rpx;
background: #0074ff;
}
.album {
width: 330rpx;
height: 240rpx;
}
.exam {
margin-top: 80rpx;
background: #dde6f6;
padding: 8rpx 30rpx;
width: fit-content;
}
</style>

View File

@ -1,80 +1,93 @@
<template> <template>
<view class="work-page"> <full-page :showSafeArea="false" :customScroll="true">
<!-- 顶部用户信息区域 --> <template #header>
<view class="user-header bg-white px-15 py-15"> <view class="user-header bg-white px-15 py-15">
<view class="flex items-center justify-between"> <view class="flex items-center justify-between" @click="editProfile()">
<!-- 左侧用户头像和信息 --> <view class="flex items-center flex-grow">
<view class="flex items-center flex-grow"> <view class="relative user-avatar mr-10">
<view class="user-avatar mr-10"> <image v-if="doctorInfo && doctorInfo.avatar" class="avatar-img rounded-full overflow-hidden"
<image :src="doctorInfo.avatar" mode="aspectFill" />
class="avatar-img" <image v-else class="avatar-img rounded-full overflow-hidden" src="/static/default-avatar.png"
src="/static/default-avatar.png" mode="aspectFill" />
mode="aspectFill" <view v-if="doctorInfo" class="edit-sub flex items-center justify-center rounded-full bg-primary">
/> <image class="edit-icon" src="/static/work/pen.svg" mode="aspectFill" />
</view>
<view class="flex-col">
<text class="user-name text-black text-lg font-semibold"
>请完善信息</text
>
<view class="flex items-center mt-5">
<view
class="status-tag tag-orange mr-10"
@click="handleCompleteInfo"
>
<text class="tag-text text-white">信息待完善</text>
</view> </view>
<view class="status-tag tag-gray" @click="handleVerify"> </view>
<text class="tag-text text-dark">未认证</text> <view class="flex-col">
<text v-if="doctorInfo && doctorInfo.anotherName" class="user-name text-dark text-lg font-semibold">
{{ doctorInfo.anotherName }}
</text>
<text v-else class="user-name text-black text-lg font-semibold">请完善信息</text>
<view class="flex items-center mt-5">
<view v-if="!doctorInfo || !doctorInfo.anotherName" class="status-tag tag-orange mr-10">
<text class="tag-text text-white">信息待完善</text>
</view>
<view v-if="certStatus" class="px-10 py-3 text-sm rounded-full" :class="certStatus.classnames"
@click.stop="toCert()">
{{ certStatus.text }}
</view>
</view> </view>
</view> </view>
</view> </view>
</view>
<!-- 右侧操作按钮 --> <!-- 右侧操作按钮 -->
<view class="flex items-center"> <view class="flex items-center">
<view <view class="action-btn flex-col items-center mr-10" @click="handleInvitePatient">
class="action-btn flex-col items-center mr-15" <image class="mb-5 qrcode-icon" src="/static/work/qrcode.svg" />
@click="handleInvitePatient" <text class="action-text text-dark text-sm">邀请</text>
> </view>
<view class="qrcode-icon"> <view class="action-btn flex-col items-center" @click="handleMore">
<text class="qrcode-text"></text> <image class="mb-5 qrcode-icon" src="/static/work/more.svg" />
<text class="action-text text-dark text-sm">更多</text>
</view> </view>
<text class="action-text text-black text-sm mt-5">邀请患者</text>
</view>
<view class="more-btn" @click="handleMore">
<text class="more-icon"></text>
</view> </view>
</view> </view>
</view> </view>
</view> <view class="mt-15 px-15 py-12 text-dark text-lg font-semibold bg-white border-b">
待办列表11
<!-- 待办列表区域 -->
<view class="todo-section px-15 mt-15">
<view class="section-title mb-15">
<text class="text-black text-lg font-semibold">待办列表11</text>
</view> </view>
</template>
<!-- 空状态 --> <scroll-view v-if="list.length" scroll-y="true" class="h-full bg-white">
<view class="empty-state"> <view class="p-15">
<empty-data text="暂无记录" /> <view v-for="i in 10" class="p-15 bg-primary mb-10"></view>
</view> </view>
</scroll-view>
<view v-else class="flex flex-col items-center justify-center h-full bg-white">
<empty-data text="暂无记录" />
</view> </view>
</view> <template #footer>
<view class="border-b"></view>
</template>
</full-page>
<cert-popup :visible="visible" @close="visible = false" />
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue';
import { storeToRefs } from "pinia";
import useGuard from "@/hooks/useGuard.js"; import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import certPopup from "./components/cert-popup.vue";
import EmptyData from "@/components/empty-data.vue"; import EmptyData from "@/components/empty-data.vue";
import fullPage from '@/components/full-page.vue';
const { useLoad } = useGuard(); const certConfig = {
verified: { text: '已认证', classnames: 'bg-success text-white' },
// verifing: { text: '认证中', classnames: 'bg-orange text-white' },
const handleCompleteInfo = () => { unverified: { text: '未认证', classnames: 'bg-gray text-dark' },
uni.navigateTo({
url: "/pages/work/profile",
});
}; };
const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore();
const { doctorInfo } = storeToRefs(useAccountStore());
const list = ref([1]);
const visible = ref(false);
const certStatus = computed(() => doctorInfo.value?.verifyStatus ? certConfig[doctorInfo.value.verifyStatus] : certConfig.unverified)
// //
const handleVerify = () => { const handleVerify = () => {
uni.showToast({ uni.showToast({
@ -101,15 +114,39 @@ const handleMore = () => {
}); });
}; };
function editProfile() {
uni.navigateTo({
url: "/pages/work/profile",
});
}
function toCert() {
// if (doctorInfo.value.verifyStatus === 'unverified') {
visible.value = true
// }
}
useLoad(() => { useLoad(() => {
console.log("工作台页面加载"); console.log("工作台页面加载");
}); });
useShow(() => {
getDoctorInfo()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.work-page { .edit-sub {
min-height: 100vh; width: 36rpx;
background: #f5f5f5; height: 36rpx;
position: absolute;
right: 0;
bottom: 0;
}
.edit-icon {
width: 24rpx;
height: 24rpx;
} }
.user-header { .user-header {
@ -119,9 +156,6 @@ useLoad(() => {
.user-avatar { .user-avatar {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 50%;
overflow: hidden;
background: #e5e5e5;
.avatar-img { .avatar-img {
width: 100%; width: 100%;
@ -133,6 +167,11 @@ useLoad(() => {
line-height: 1.5; line-height: 1.5;
} }
.py-3 {
padding-top: 6rpx;
padding-bottom: 6rpx;
}
.status-tag { .status-tag {
padding: 6rpx 16rpx; padding: 6rpx 16rpx;
border-radius: 20rpx; border-radius: 20rpx;
@ -162,17 +201,8 @@ useLoad(() => {
justify-content: center; justify-content: center;
.qrcode-icon { .qrcode-icon {
width: 48rpx; width: 40rpx;
height: 48rpx; height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
.qrcode-text {
font-size: 40rpx;
color: #333;
line-height: 1;
}
} }
.action-text { .action-text {
@ -209,5 +239,4 @@ useLoad(() => {
// align-items: center; // align-items: center;
// justify-content: center; // justify-content: center;
// width: 100%; // width: 100%;
// } // }</style>
</style>

View File

@ -1,20 +1,36 @@
export default [ export default [
{ {
path: 'pages/message/message', path: 'pages/message/message',
meta: { title: '首页', login: false }, meta: { title: '消息' },
style: { navigationStyle: 'custom' } style: { navigationStyle: 'custom' }
}, },
{ {
path: 'pages/work/work', path: 'pages/work/work',
meta: { title: '工作台', login: false } meta: { title: '工作台' }
},
{
path: 'pages/case/case',
meta: { title: '病例' }
}, },
{ {
path: 'pages/work/profile', path: 'pages/work/profile',
meta: { title: '完善个人信息', login: false } meta: { title: '完善个人信息' }
}, },
{ {
path: 'pages/work/department-select', path: 'pages/work/department-select',
meta: { title: '选择科室', login: false } meta: { title: '选择科室' }
} },
{
path: 'pages/work/verify/assistant',
meta: { title: '上传证照', login: true }
},
{
path: 'pages/work/verify/doctor',
meta: { title: '上传证照', login: true }
},
{
path: 'pages/login/login',
meta: { title: '授权登录' }
},
] ]

BIN
static/work/aIDCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
static/work/camera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/work/cardBack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
static/work/cardFront.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
static/work/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

BIN
static/work/fIDCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
static/work/flashCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
static/work/hook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/work/lackCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
static/work/licenseBack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

1
static/work/more.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769069890058" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14332" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M378.112 481.536H159.488c-51.2 0-92.928-41.728-92.928-92.928V169.984c0-51.2 41.728-92.928 92.928-92.928h218.624c51.2 0 92.928 41.728 92.928 92.928v218.624c0 51.2-41.728 92.928-92.928 92.928zM159.488 138.752c-17.408 0-31.488 14.08-31.488 31.488v218.624c0 17.408 14.08 31.488 31.488 31.488h218.624c17.408 0 31.488-14.08 31.488-31.488V169.984c0-17.408-14.08-31.488-31.488-31.488H159.488zM378.112 949.504H159.488c-51.2 0-92.928-41.728-92.928-92.928v-218.624c0-51.2 41.728-92.928 92.928-92.928h218.624c51.2 0 92.928 41.728 92.928 92.928v218.624c0 51.456-41.728 92.928-92.928 92.928zM159.488 606.72c-17.408 0-31.488 14.08-31.488 31.488v218.624c0 17.408 14.08 31.488 31.488 31.488h218.624c17.408 0 31.488-14.08 31.488-31.488v-218.624c0-17.408-14.08-31.488-31.488-31.488H159.488zM680.448 480l-143.616-144.128c-33.536-33.792-33.536-88.576 0.256-122.112l144.128-143.616c33.792-33.536 88.576-33.536 122.112 0.256l143.616 144.128c33.536 33.792 33.536 88.576-0.256 122.112l-144.128 143.616c-33.792 33.536-88.32 33.536-122.112-0.256zM762.88 110.848c-11.264-11.52-29.952-11.52-41.216 0l-144.128 143.616c-11.52 11.264-11.52 29.952 0 41.216l143.616 144.128c11.264 11.52 29.952 11.52 41.216 0l144.128-143.616c11.52-11.264 11.52-29.952 0-41.216L762.88 110.848zM840.96 949.504l-218.624-0.512c-51.2 0-92.672-41.728-92.672-92.928l0.512-218.624c0-51.2 41.728-92.672 92.928-92.672l218.624 0.512c51.2 0 92.672 41.728 92.672 92.928l-0.512 218.624c0 51.2-41.728 92.928-92.928 92.672z m-217.856-343.04c-17.408 0-31.488 14.08-31.488 31.232l-0.512 218.624c0 17.408 14.08 31.488 31.232 31.488l218.624 0.512c17.408 0 31.488-14.08 31.488-31.232l0.512-218.624c0-17.408-14.08-31.488-31.232-31.488l-218.624-0.512z" fill="#2c2c2c" p-id="14333"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

1
static/work/pen.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769067872142" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10445" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M581.50172 186.48l256.04 256.04-555.98 555.98-228.28 25.2C22.72172 1027.08-3.09828 1001.24 0.30172 970.68l25.4-228.44 555.8-555.76z m414.4-38.12l-120.22-120.22c-37.5-37.5-98.32-37.5-135.82 0l-113.1 113.1 256.04 256.04 113.1-113.1c37.5-37.52 37.5-98.32 0-135.82z" fill="#ffffff" p-id="10446"></path></svg>

After

Width:  |  Height:  |  Size: 638 B

1
static/work/qrcode.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769069475491" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11505" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M85.312 85.312V384H384V85.312H85.312zM0 0h469.248v469.248H0V0z m170.624 170.624h128v128h-128v-128zM0 554.624h469.248v469.248H0V554.624z m85.312 85.312v298.624H384V639.936H85.312z m85.312 85.312h128v128h-128v-128zM554.624 0h469.248v469.248H554.624V0z m85.312 85.312V384h298.624V85.312H639.936z m383.936 682.56H1024v85.376h-298.752V639.936H639.936V1023.872H554.624V554.624h255.936v213.248h128V554.624h85.312v213.248z m-298.624-597.248h128v128h-128v-128z m298.624 853.248h-85.312v-85.312h85.312v85.312z m-213.312 0h-85.312v-85.312h85.312v85.312z" fill="#262626" p-id="11506"></path></svg>

After

Width:  |  Height:  |  Size: 919 B

BIN
static/work/vagueCard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -8,15 +8,27 @@ const env = __VITE_ENV__;
export default defineStore("accountStore", () => { export default defineStore("accountStore", () => {
const appid = env.MP_WX_APP_ID; const appid = env.MP_WX_APP_ID;
const corpId = env.MP_CORP_ID;
const account = ref(null); const account = ref(null);
const loading = ref(false) const loading = ref(false);
const loginPromise = ref(null);
// IM 相关 // IM 相关
const openid = ref(""); const openid = ref("");
// 医生信息 // 医生信息
const doctorInfo = ref(null); const doctorInfo = ref(null);
async function login(phoneCode = '') {
if (loading.value) return; function getLoginPromise(phoneCode = '') {
loading.value = true; if (loginPromise.value) return loginPromise.value;
loginPromise.value = loginByCode(phoneCode);
return loginPromise.value;
}
async function login(phoneCode) {
await getLoginPromise(phoneCode);
loginPromise.value = null;
}
async function loginByCode(phoneCode = '') {
try { try {
const { code } = await uni.login({ const { code } = await uni.login({
appid, appid,
@ -27,8 +39,8 @@ export default defineStore("accountStore", () => {
const res = await api('wxAppLogin', { const res = await api('wxAppLogin', {
phoneCode, phoneCode,
code, code,
corpId,
}); });
loading.value = false;
if (res.success && res.data) { if (res.success && res.data) {
if (!res.data.mobile) { if (!res.data.mobile) {
const target = '/pages/login/login'; const target = '/pages/login/login';
@ -42,11 +54,13 @@ export default defineStore("accountStore", () => {
} }
} }
toast('登录失败,请重新登录'); toast('登录失败,请重新登录');
} catch (e) { } catch (e) {
toast('登录失败,请重新登录'); toast('登录失败,请重新登录');
} }
loading.value = false return Promise.reject()
} }
async function getDoctorInfo() { async function getDoctorInfo() {
try { try {
const res = await api('getCorpMemberData', { const res = await api('getCorpMemberData', {

View File

@ -11,7 +11,9 @@ const urlsConfig = {
getHospitalList: 'getRealHospital', getHospitalList: 'getRealHospital',
addCorpMember: 'addCorpMember', addCorpMember: 'addCorpMember',
getCorpMemberData: 'getCorpMemberData', getCorpMemberData: 'getCorpMemberData',
updateCorpMember: 'updateCorpMember' updateCorpMember: 'updateCorpMember',
addCorpMemberFromWxapp: "addCorpMemberFromWxapp",
updateCorpMemberFromWxapp: "updateCorpMemberFromWxapp"
}, },
knowledgeBase: { knowledgeBase: {

View File

@ -21,21 +21,21 @@ let retryQueue = [];
* @param {String} token - 新的 token * @param {String} token - 新的 token
*/ */
function processQueue(token) { function processQueue(token) {
retryQueue.forEach(({ resolve, reject, config }) => { retryQueue.forEach(({ resolve, reject, config }) => {
if (token) { if (token) {
// 更新 token 并重试请求 // 更新 token 并重试请求
if (!config.header) config.header = {}; if (!config.header) config.header = {};
config.header.Authorization = `Bearer ${token}`; config.header.Authorization = `Bearer ${token}`;
uni.request({ uni.request({
...config, ...config,
success: (res) => resolve(res), success: (res) => resolve(res),
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
} else { } else {
reject(new Error("Token 刷新失败")); reject(new Error("Token 刷新失败"));
} }
}); });
retryQueue = []; retryQueue = [];
} }
/** /**
@ -43,114 +43,114 @@ function processQueue(token) {
* @returns {Promise<String|null>} * @returns {Promise<String|null>}
*/ */
async function refreshAccessToken() { async function refreshAccessToken() {
const refreshToken = uni.getStorageSync("refreshToken"); const refreshToken = uni.getStorageSync("refreshToken");
if (!refreshToken) { if (!refreshToken) {
return null; return null;
} }
if (isRefreshing) { if (isRefreshing) {
// 等待刷新完成 // 等待刷新完成
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
return uni.getStorageSync("accessToken"); return uni.getStorageSync("accessToken");
} }
isRefreshing = true; isRefreshing = true;
try { try {
const res = await uni.request({ const res = await uni.request({
url: `${baseUrl}/auth/refresh`, url: `${baseUrl}/auth/refresh`,
method: "POST", method: "POST",
header: { header: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
data: { data: {
refreshToken, refreshToken,
}, },
}); });
if (res && res.data && res.data.success && res.data.data) { if (res && res.data && res.data.success && res.data.data) {
const newToken = res.data.data.accessToken; const newToken = res.data.data.accessToken;
uni.setStorageSync("accessToken", newToken); uni.setStorageSync("accessToken", newToken);
processQueue(newToken); processQueue(newToken);
return newToken; return newToken;
} else { } else {
// 刷新失败,清空队列 // 刷新失败,清空队列
processQueue(null); processQueue(null);
// 清除登录状态 // 清除登录状态
uni.removeStorageSync("accessToken"); uni.removeStorageSync("accessToken");
uni.removeStorageSync("refreshToken"); uni.removeStorageSync("refreshToken");
uni.removeStorageSync("jwtUserInfo"); uni.removeStorageSync("jwtUserInfo");
return null; return null;
} }
} catch (error) { } catch (error) {
console.error("Token 刷新异常:", error); console.error("Token 刷新异常:", error);
processQueue(null); processQueue(null);
uni.removeStorageSync("accessToken"); uni.removeStorageSync("accessToken");
uni.removeStorageSync("refreshToken"); uni.removeStorageSync("refreshToken");
uni.removeStorageSync("jwtUserInfo"); uni.removeStorageSync("jwtUserInfo");
return null; return null;
} finally { } finally {
isRefreshing = false; isRefreshing = false;
} }
} }
const request = async (options = {}, showLoading = true) => { const request = async (options = {}, showLoading = true) => {
// 合并用户传入的配置和默认配置 // 合并用户传入的配置和默认配置
if (!options.data) options.data = {}; if (!options.data) options.data = {};
if(!options.data.corpId) { if (!options.data.corpId) {
options.data.corpId = env.MP_CORP_ID; options.data.corpId = env.MP_CORP_ID;
} }
const config = { const config = {
...defaultOptions, ...defaultOptions,
...options, ...options,
url: baseUrl + options.url, url: baseUrl + options.url,
}; };
// 添加 token 到请求头 // 添加 token 到请求头
// const accessToken = uni.getStorageSync("accessToken"); // const accessToken = uni.getStorageSync("accessToken");
// if (accessToken) { // if (accessToken) {
// if (!config.header) config.header = {}; // if (!config.header) config.header = {};
// config.header.Authorization = `Bearer ${accessToken}`; // config.header.Authorization = `Bearer ${accessToken}`;
// } // }
const key = `${JSON.stringify(config)}_${Date.now()}`; const key = `${JSON.stringify(config)}_${Date.now()}`;
if (showLoading) { if (showLoading) {
recordTask(key) recordTask(key)
} }
try { try {
// 发起请求 // 发起请求
const res = await uni.request(config); const res = await uni.request(config);
if (showLoading) { if (showLoading) {
removeTask(key) removeTask(key)
} }
// 如果返回 401尝试刷新 token // 如果返回 401尝试刷新 token
// if (res.statusCode === 401) { // if (res.statusCode === 401) {
// if (showLoading) { // if (showLoading) {
// removeTask(key) // removeTask(key)
// } // }
// // 尝试刷新 token // // 尝试刷新 token
// const newToken = await refreshAccessToken(); // const newToken = await refreshAccessToken();
// if (newToken) { // if (newToken) {
// // 刷新成功,重试原请求 // // 刷新成功,重试原请求
// if (!config.header) config.header = {}; // if (!config.header) config.header = {};
// config.header.Authorization = `Bearer ${newToken}`; // config.header.Authorization = `Bearer ${newToken}`;
// if (showLoading) { // if (showLoading) {
// recordTask(key) // recordTask(key)
// } // }
// const retryRes = await uni.request(config); // const retryRes = await uni.request(config);
// if (showLoading) { // if (showLoading) {
// removeTask(key) // removeTask(key)
// } // }
// const success = retryRes && retryRes.data && retryRes.data.success === true; // const success = retryRes && retryRes.data && retryRes.data.success === true;
// if (success) { // if (success) {
// return retryRes.data; // return retryRes.data;
@ -166,7 +166,7 @@ const request = async (options = {}, showLoading = true) => {
// }; // };
// } // }
// } // }
const success = res && res.data && res.data.success === true; const success = res && res.data && res.data.success === true;
if (success) { if (success) {
return res.data; return res.data;
@ -206,5 +206,28 @@ export default request;
export const uploadUrl = `${baseUrl}/upload`; export const uploadUrl = `${baseUrl}/upload`;
export function getFullPath(path) { export function getFullPath(path) {
return `${baseUrl}${path}`; return `${baseUrl}/${path}`;
} }
export function upload(path) {
return new Promise((resolve) => {
uni.uploadFile({
url: uploadUrl, // 替换为你的上传接口地址
filePath: path,
name: 'file',
fileType: 'image',
success: (res) => {
try {
const url = JSON.parse(res.data).filePath;
resolve(url ? getFullPath(url) : '')
} catch (e) {
resolve()
}
},
fail: res => {
resolve()
}
})
})
}

View File

@ -1,7 +1,7 @@
// 引入腾讯IM SDK // 引入腾讯IM SDK
import TIM from 'tim-wx-sdk' import TIM from 'tim-wx-sdk'
import TIMUploadPlugin from 'tim-upload-plugin' import TIMUploadPlugin from 'tim-upload-plugin'
import { getUserSig, sendSystemMessage, getChatRecordsByGroupId } from '../api/corp/im.js' // import { getUserSig, sendSystemMessage, getChatRecordsByGroupId } from '../api/corp/im.js'
const env = __VITE_ENV__; const env = __VITE_ENV__;