# Conflicts:
#	App.vue
#	pages.json
This commit is contained in:
wangdongbo 2026-01-23 16:41:39 +08:00
commit 921ee6c7d4
35 changed files with 1892 additions and 1044 deletions

View File

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

737
App.vue
View File

@ -1,369 +1,368 @@
<script>
import useAccountStore from "@/store/account.js";
import {
globalTimChatManager
} from "@/utils/tim-chat.js";
export default {
onLaunch: function() {
// pinia store getActivePinia
const {
login
} = useAccountStore();
login();
console.log("App Launch: ");
},
onShow: function() {
console.log("App Show");
},
onHide: function() {
console.log("App Hide");
try {
if (globalTimChatManager && globalTimChatManager.tim) {
console.log('小程序退出开始退出腾讯IM');
globalTimChatManager.destroy();
console.log('腾讯IM退出成功');
}
} catch (error) {
console.error('退出腾讯IM失败:', error);
}
},
};
</script>
<style lang="scss">
$primary-color: #0074ff;
page {
height: 100%;
background: #f5f5f5;
}
.shadow-up {
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
rgba(0, 0, 0, 0.1) 0px -10px 15px -3px, rgba(0, 0, 0, 0.1) 0px -4px 6px -4px;
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.relative {
position: relative;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-grow {
flex-grow: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.justify-end {
justify-content: flex-end;
}
.bg-gray {
background: #f5f5f5;
}
.bg-primary {
background: $primary-color;
}
.bg-success {
background: green;
}
.bg-white {
background: #fff;
}
.bg-warning {
background: orange;
}
.bg-danger {
background: rgb(248 113 113);
}
.py-5 {
padding-top: 10rpx;
padding-bottom: 10rpx;
}
.p-15 {
padding: 30rpx;
}
.p-10 {
padding: 20rpx;
}
.px-5 {
padding-left: 10rpx;
padding-right: 10rpx;
}
.px-10 {
padding-left: 20rpx;
padding-right: 20rpx;
}
.px-12 {
padding-left: 24rpx;
padding-right: 24rpx;
}
.px-15 {
padding-left: 30rpx;
padding-right: 30rpx;
}
.pt-5 {
padding-top: 10rpx;
}
.pt-15 {
padding-top: 30rpx;
}
.break-all {
word-break: break-all;
}
.pb-5 {
padding-bottom: 10rpx;
}
.pb-10 {
padding-bottom: 20rpx;
}
.py-10 {
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.py-12 {
padding-top: 24rpx;
padding-bottom: 24rpx;
}
.py-15 {
padding-top: 30rpx;
padding-bottom: 30rpx;
}
.mr-5 {
margin-right: 10rpx;
}
.mr-10 {
margin-right: 20rpx;
}
.ml-15 {
margin-left: 30rpx;
}
.mb-5 {
margin-bottom: 10rpx;
}
.mb-10 {
margin-bottom: 20rpx;
}
.mt-10 {
margin-top: 20rpx;
}
.mt-15 {
margin-top: 30rpx;
}
.mt-12 {
margin-top: 24rpx;
}
.mx-10 {
margin-left: 20rpx;
margin-right: 20rpx;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.rounded {
border-radius: 16rpx;
}
.rounded-sm {
border-radius: 8rpx;
}
.rounded-full {
border-radius: 999px;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-gray {
color: #999;
}
.text-black {
color: #000;
}
.text-dark {
color: #333;
}
.text-danger {
color: #f56c6c;
}
.text-primary {
color: $primary-color;
}
.text-success {
color: #67c23a;
}
.text-white {
color: #fff;
}
.text-warning {
color: #f56c6c;
}
.text-sm {
font-size: 24rpx;
}
.text-base {
font-size: 28rpx;
}
.text-lg {
font-size: 32rpx;
}
.text-xl {
font-size: 36rpx;
}
.leading-normal {
line-height: 1.5;
}
.border {
border: 1px solid #eee;
}
.border-dashed {
border: 1px dashed #eee;
}
.border-b {
border-bottom: 1px solid #eee;
}
.border-auto {
border: 1px solid;
}
.border-dashed-auto {
border: 1px dashed;
}
.border-primary {
border: 1px solid $primary-color;
}
.font-semibold {
font-weight: bold;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.w-0 {
width: 0;
}
.w-full {
width: 100%;
}
.overflow-hidden {
overflow: hidden;
}
.border-box {
box-sizing: border-box;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.h-full {
height: 100%;
}
.safe-bottom-padding {
height: env(safe-area-inset-bottom);
}
</style>
<script>
import useAccountStore from "@/store/account.js";
import { globalTimChatManager } from "@/utils/tim-chat.js";
export default {
onLaunch: function () {
// pinia store getActivePinia
const { login } = useAccountStore();
login();
console.log("App Launch: ");
},
onShow: function () {
console.log("App Show");
},
onHide: function () {
console.log("App Hide");
// 退退IM
// try {
// if (globalTimChatManager && globalTimChatManager.tim) {
// console.log('退退IM');
// globalTimChatManager.destroy();
// console.log('IM退');
// }
// } catch (error) {
// console.error('退IM:', error);
// }
},
};
</script>
<style lang="scss">
$primary-color: #0074ff;
page {
height: 100%;
background: #f5f5f5;
}
.shadow-up {
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
rgba(0, 0, 0, 0.1) 0px -10px 15px -3px, rgba(0, 0, 0, 0.1) 0px -4px 6px -4px;
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.relative {
position: relative;
}
.absolute{
position: absolute;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-grow {
flex-grow: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.justify-end {
justify-content: flex-end;
}
.bg-gray {
background: #f5f5f5;
}
.bg-primary {
background: $primary-color;
}
.bg-success {
background: green;
}
.bg-white {
background: #fff;
}
.bg-warning {
background: orange;
}
.bg-danger {
background: rgb(248 113 113);
}
.py-5 {
padding-top: 10rpx;
padding-bottom: 10rpx;
}
.p-15 {
padding: 30rpx;
}
.p-10 {
padding: 20rpx;
}
.px-5 {
padding-left: 10rpx;
padding-right: 10rpx;
}
.px-10 {
padding-left: 20rpx;
padding-right: 20rpx;
}
.px-12 {
padding-left: 24rpx;
padding-right: 24rpx;
}
.px-15 {
padding-left: 30rpx;
padding-right: 30rpx;
}
.pt-5 {
padding-top: 10rpx;
}
.pt-15 {
padding-top: 30rpx;
}
.break-all {
word-break: break-all;
}
.pb-5 {
padding-bottom: 10rpx;
}
.pb-10 {
padding-bottom: 20rpx;
}
.py-10 {
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.py-12 {
padding-top: 24rpx;
padding-bottom: 24rpx;
}
.py-15 {
padding-top: 30rpx;
padding-bottom: 30rpx;
}
.mr-5 {
margin-right: 10rpx;
}
.mr-10 {
margin-right: 20rpx;
}
.ml-15 {
margin-left: 30rpx;
}
.mb-5 {
margin-bottom: 10rpx;
}
.mb-10 {
margin-bottom: 20rpx;
}
.mt-10 {
margin-top: 20rpx;
}
.mt-15 {
margin-top: 30rpx;
}
.mt-12 {
margin-top: 24rpx;
}
.mx-10 {
margin-left: 20rpx;
margin-right: 20rpx;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.rounded {
border-radius: 16rpx;
}
.rounded-sm {
border-radius: 8rpx;
}
.rounded-full {
border-radius: 999px;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-gray {
color: #999;
}
.text-black {
color: #000;
}
.text-dark {
color: #333;
}
.text-danger {
color: #f56c6c;
}
.text-primary {
color: $primary-color;
}
.text-success {
color: #67c23a;
}
.text-white {
color: #fff;
}
.text-warning {
color: #f56c6c;
}
.text-sm {
font-size: 24rpx;
}
.text-base {
font-size: 28rpx;
}
.text-lg {
font-size: 32rpx;
}
.text-xl {
font-size: 36rpx;
}
.leading-normal {
line-height: 1.5;
}
.border {
border: 1px solid #eee;
}
.border-dashed {
border: 1px dashed #eee;
}
.border-b {
border-bottom: 1px solid #eee;
}
.border-auto {
border: 1px solid;
}
.border-dashed-auto {
border: 1px dashed;
}
.border-primary {
border: 1px solid $primary-color;
}
.font-semibold {
font-weight: bold;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.w-0 {
width: 0;
}
.w-full {
width: 100%;
}
.overflow-hidden {
overflow: hidden;
}
.border-box {
box-sizing: border-box;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.h-full {
height: 100%;
}
.safe-bottom-padding {
height: env(safe-area-inset-bottom);
}
</style>

View File

@ -1,5 +1,5 @@
<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'">
<view v-if="showCancel" class="flex-grow py-10 text-base border-primary rounded text-primary rounded"
@click="cancel()">

View File

@ -1,5 +1,5 @@
<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">
<view class="form-content__wrapper">
<view class="flex-main-content truncate" :class="value ? '' : 'form__placeholder'">
@ -49,9 +49,9 @@ const displayRange = computed(() => {
}
return props.range;
})
const value = computed(() => {
if (!props.form || !props.form[props.title]) return '';
const currentValue = props.form[props.title];
// rangelabel
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];
emits('change', {
title: props.title,
value: selectedValue
value: typeof selectedValue === 'object' ? selectedValue.value : selectedValue
})
}
</script>

View File

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

View File

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

View File

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

View File

@ -19,18 +19,18 @@
"navigationBarTitleText": "常用语"
}
},
{
"path": "pages/case/case",
"style": {
"navigationBarTitleText": "病例"
}
},
{
"path": "pages/work/work",
"style": {
"navigationBarTitleText": "工作台"
}
},
{
"path": "pages/case/case",
"style": {
"navigationBarTitleText": "病例"
}
},
{
"path": "pages/work/profile",
"style": {
@ -43,10 +43,22 @@
"navigationBarTitleText": "选择科室"
}
},
{
"path": "pages/work/verify/assistant",
"style": {
"navigationBarTitleText": "上传证照"
}
},
{
"path": "pages/work/verify/doctor",
"style": {
"navigationBarTitleText": "上传证照"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
"navigationBarTitleText": "授权登录"
}
}
],

View File

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

File diff suppressed because it is too large Load Diff

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>
<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"
/>
<full-page>
<view class="p-15">
<view class="bg-white px-10 mb-10 rounded">
<form-input :form="formData" required wordLimit="10" title="anotherName" name="姓名" @change="onChange($event)" />
<common-cell title="avatar" name="头像">
<view class="flex-grow flex items-center justify-end" @click="chooseAvatar()">
<image v-if="formData.avatar" class="avatar mr-5 rounded-full" :src="formData.avatar" />
<image v-else class="avatar mr-5 rounded-full" src="/static/default-avatar.png" />
<uni-icons color="#999" type="right" size="16" />
</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>
</common-cell>
<form-select :form="formData" name="性别" title="gender" :range="genderOptions" @change="onChange($event)" />
<form-input :form="formData" disableChange wordLimit="11" title="mobile" name="手机号 (不可修改)" />
</view>
<!-- 个人介绍 -->
<form-textarea
name="个人介绍"
:form="formData"
title="intro"
:word-limit="500"
@change="handleFieldChange"
/>
<view class="bg-white px-10 mb-10 rounded">
<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>
<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 class="button-footer">
<view class="btn btn-cancel" @click="handleCancel">取消</view>
<view class="btn btn-save" @click="handleSave">保存</view>
</view>
</view>
<template #footer>
<button-footer :cancelText="cancelText" :confirmText="confirmText" @confirm="save()" @cancel="back()" />
</template>
</full-page>
</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 { computed, ref } from "vue";
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 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";
import fullPage from '@/components/full-page.vue';
const { account, doctorInfo } = storeToRefs(useAccountStore());
const { useLoad } = useGuard();
const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore();
//
const formData = ref({
anotherName: "",
avatar: "",
gender: "",
mobile: "",
position: "",
title: "",
department: "",
departmentName: "",
departmentId: "",
intro: "",
});
const form = ref({});
const type = ref('')
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value, mobile: account.value?.mobile }));
const cancelText = computed(() => doctorInfo.value ? '取消' : '暂不填写');
const confirmText = computed(() => type.value === 'cert' ? '下一步' : '保存');
//
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({
@ -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>
<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;
}
.avatar {
width: 64rpx;
height: 64rpx;
}
</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>
<view class="work-page">
<!-- 顶部用户信息区域 -->
<view class="user-header bg-white px-15 py-15">
<view class="flex items-center justify-between">
<!-- 左侧用户头像和信息 -->
<view class="flex items-center flex-grow">
<view class="user-avatar mr-10">
<image
class="avatar-img"
src="/static/default-avatar.png"
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>
<full-page :showSafeArea="false" :customScroll="true">
<template #header>
<view class="user-header bg-white px-15 py-15">
<view class="flex items-center justify-between" @click="editProfile()">
<view class="flex items-center flex-grow">
<view class="relative user-avatar mr-10">
<image v-if="doctorInfo && doctorInfo.avatar" class="avatar-img rounded-full overflow-hidden"
:src="doctorInfo.avatar" mode="aspectFill" />
<image v-else class="avatar-img rounded-full overflow-hidden" src="/static/default-avatar.png"
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="status-tag tag-gray" @click="handleVerify">
<text class="tag-text text-dark">未认证</text>
</view>
<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 class="flex items-center">
<view
class="action-btn flex-col items-center mr-15"
@click="handleInvitePatient"
>
<view class="qrcode-icon">
<text class="qrcode-text"></text>
<!-- 右侧操作按钮 -->
<view class="flex items-center">
<view class="action-btn flex-col items-center mr-10" @click="handleInvitePatient">
<image class="mb-5 qrcode-icon" src="/static/work/qrcode.svg" />
<text class="action-text text-dark text-sm">邀请</text>
</view>
<view class="action-btn flex-col items-center" @click="handleMore">
<image class="mb-5 qrcode-icon" src="/static/work/more.svg" />
<text class="action-text text-dark text-sm">更多</text>
</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 class="todo-section px-15 mt-15">
<view class="section-title mb-15">
<text class="text-black text-lg font-semibold">待办列表11</text>
<view class="mt-15 px-15 py-12 text-dark text-lg font-semibold bg-white border-b">
待办列表11
</view>
</template>
<!-- 空状态 -->
<view class="empty-state">
<empty-data text="暂无记录" />
<scroll-view v-if="list.length" scroll-y="true" class="h-full bg-white">
<view class="p-15">
<view v-for="i in 10" class="p-15 bg-primary mb-10"></view>
</view>
</scroll-view>
<view v-else class="flex flex-col items-center justify-center h-full bg-white">
<empty-data text="暂无记录" />
</view>
</view>
<template #footer>
<view class="border-b"></view>
</template>
</full-page>
<cert-popup :visible="visible" @close="visible = false" />
</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 certPopup from "./components/cert-popup.vue";
import EmptyData from "@/components/empty-data.vue";
import fullPage from '@/components/full-page.vue';
const { useLoad } = useGuard();
//
const handleCompleteInfo = () => {
uni.navigateTo({
url: "/pages/work/profile",
});
const certConfig = {
verified: { text: '已认证', classnames: 'bg-success text-white' },
verifing: { text: '认证中', classnames: 'bg-orange text-white' },
unverified: { text: '未认证', classnames: 'bg-gray text-dark' },
};
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 = () => {
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(() => {
console.log("工作台页面加载");
});
useShow(() => {
getDoctorInfo()
})
</script>
<style lang="scss" scoped>
.work-page {
min-height: 100vh;
background: #f5f5f5;
.edit-sub {
width: 36rpx;
height: 36rpx;
position: absolute;
right: 0;
bottom: 0;
}
.edit-icon {
width: 24rpx;
height: 24rpx;
}
.user-header {
@ -119,9 +156,6 @@ useLoad(() => {
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
background: #e5e5e5;
.avatar-img {
width: 100%;
@ -133,6 +167,11 @@ useLoad(() => {
line-height: 1.5;
}
.py-3 {
padding-top: 6rpx;
padding-bottom: 6rpx;
}
.status-tag {
padding: 6rpx 16rpx;
border-radius: 20rpx;
@ -162,17 +201,8 @@ useLoad(() => {
justify-content: center;
.qrcode-icon {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.qrcode-text {
font-size: 40rpx;
color: #333;
line-height: 1;
}
width: 40rpx;
height: 40rpx;
}
.action-text {
@ -209,5 +239,4 @@ useLoad(() => {
// align-items: center;
// justify-content: center;
// width: 100%;
// }
</style>
// }</style>

View File

@ -1,20 +1,40 @@
export default [
{
path: 'pages/message/message',
meta: { title: '首页', login: false },
style: { navigationStyle: 'custom' }
meta: { title: '消息' }
},
{
path: 'pages/message/index',
meta: { title: '聊天' },
style: { enablePullDownRefresh: false }
},
{
path: 'pages/work/work',
meta: { title: '工作台', login: false }
meta: { title: '工作台' }
},
{
path: 'pages/case/case',
meta: { title: '病例' }
},
{
path: 'pages/work/profile',
meta: { title: '完善个人信息', login: false }
meta: { title: '完善个人信息' }
},
{
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,17 +8,28 @@ const env = __VITE_ENV__;
export default defineStore("accountStore", () => {
const appid = env.MP_WX_APP_ID;
const corpId = env.MP_CORP_ID;
const account = ref(null);
const loading = ref(false)
const loading = ref(false);
const loginPromise = ref(null);
// IM 相关
const openid = ref("");
const isIMInitialized = ref(false);
// 医生信息
const doctorInfo = ref(null);
async function login(phoneCode = '') {
if (loading.value) return;
loading.value = true;
function getLoginPromise(phoneCode = '') {
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 {
const { code } = await uni.login({
appid,
@ -29,8 +40,8 @@ export default defineStore("accountStore", () => {
const res = await api('wxAppLogin', {
phoneCode,
code,
corpId,
});
loading.value = false;
if (res.success && res.data) {
if (!res.data.mobile) {
const target = '/pages/login/login';
@ -39,8 +50,6 @@ export default defineStore("accountStore", () => {
}
account.value = res.data;
openid.value = res.data.openid;
// 登录成功后初始化腾讯IM
try {
console.log('开始初始化腾讯IMuserID:', res.data.openid);
@ -56,10 +65,11 @@ export default defineStore("accountStore", () => {
}
}
toast('登录失败,请重新登录');
} catch (e) {
toast('登录失败,请重新登录');
}
loading.value = false
return Promise.reject()
}
async function getDoctorInfo() {

View File

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

View File

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