1. 更新切换账号逻辑
2. 诊所管理员功能上线
This commit is contained in:
53
api/doctorAuth.js
Normal file
53
api/doctorAuth.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { req } from '@/common/js/index.js';
|
||||
|
||||
const AUTH_PREFIX = '/newApi/doctor-wx-auth';
|
||||
|
||||
function unwrapResult(res) {
|
||||
if (!res) return null;
|
||||
if (res.code === 0 && res.result !== undefined) return res.result;
|
||||
return res.result != null ? res.result : res.data;
|
||||
}
|
||||
|
||||
function doctorAuthRequest(options) {
|
||||
return req.request({
|
||||
...options,
|
||||
url: `${AUTH_PREFIX}${options.url}`,
|
||||
}).then((res) => {
|
||||
const data = unwrapResult(res);
|
||||
const ok = res && res.code === 0;
|
||||
return { res, data, ok };
|
||||
});
|
||||
}
|
||||
|
||||
export function sendDoctorWxCode(phone, password) {
|
||||
return req.request({
|
||||
url: `${AUTH_PREFIX}/send-verification-code`,
|
||||
method: 'POST',
|
||||
data: { phone, password },
|
||||
header: { isToken: false },
|
||||
});
|
||||
}
|
||||
|
||||
export function doctorWxLogin(payload) {
|
||||
return req.request({
|
||||
url: `${AUTH_PREFIX}/login`,
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
header: { isToken: false },
|
||||
});
|
||||
}
|
||||
|
||||
export function getDoctorWxSiblingAccounts() {
|
||||
return doctorAuthRequest({
|
||||
url: '/sibling-accounts',
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export function switchDoctorWxAccount(payload) {
|
||||
return doctorAuthRequest({
|
||||
url: '/switch-account',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
});
|
||||
}
|
||||
@@ -21,12 +21,11 @@ export async function requestConfig(ins, options, successHandler = null, failHan
|
||||
}
|
||||
options.url = options.url.replace('/oldApi/', '');
|
||||
options.url = options.url.replace('/newApi/', '');
|
||||
// base
|
||||
ins.header = options.header || ins.header
|
||||
// config base
|
||||
const defaultHeader = { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
// 每次请求独立合并 header,避免登录 isToken:false 污染单例
|
||||
let config = {
|
||||
url:`${baseUrl}${options.url?options.url:''}`,
|
||||
header:ins.header
|
||||
header: { ...defaultHeader, ...(options.header || {}) }
|
||||
}
|
||||
if(ins.requestInterceptor){
|
||||
let _cg = null
|
||||
|
||||
@@ -40,13 +40,26 @@ const reqInterceptor = async (options) => {
|
||||
options.header = {
|
||||
...options.header
|
||||
}
|
||||
const isToken = (options.header || {}).isToken === false
|
||||
const isClinicAdminReq = (options.header || {})._clinicAdmin === '1'
|
||||
const skipToken = options.header.isToken === false
|
||||
const isClinicAdminReq = options.header._clinicAdmin === '1'
|
||||
|| (options.url && String(options.url).indexOf('clinic-admin') !== -1)
|
||||
const isDoctorWxAuthReq = options.url && String(options.url).indexOf('doctor-wx-auth') !== -1
|
||||
// 内部控制位,不得作为 HTTP 头发出
|
||||
delete options.header.isToken
|
||||
delete options.header._clinicAdmin
|
||||
const clinicToken = uni.getStorageSync('clinic_admin_token')
|
||||
const doctorToken = uni.getStorageSync('token')
|
||||
if (!isToken) {
|
||||
if (isClinicAdminReq && clinicToken) {
|
||||
const loginMode = uni.getStorageSync('loginMode')
|
||||
if (!skipToken) {
|
||||
if (isDoctorWxAuthReq) {
|
||||
if (loginMode === 'clinic_admin' && clinicToken) {
|
||||
options.header['authorization'] = `Bearer ${clinicToken}`
|
||||
} else if (doctorToken) {
|
||||
options.header['authorization'] = `Bearer ${doctorToken}`
|
||||
} else if (clinicToken) {
|
||||
options.header['authorization'] = `Bearer ${clinicToken}`
|
||||
}
|
||||
} else if (isClinicAdminReq && clinicToken) {
|
||||
options.header['authorization'] = `Bearer ${clinicToken}`
|
||||
} else if (doctorToken) {
|
||||
options.header['authorization'] = `Bearer ${doctorToken}`
|
||||
|
||||
84
components/account-switch/account-switch-entry.vue
Normal file
84
components/account-switch/account-switch-entry.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<view v-if="canSwitch">
|
||||
<view
|
||||
class="switch-row"
|
||||
hover-class="switch-row-hover"
|
||||
:hover-stay-time="100"
|
||||
@tap.stop="openSwitch"
|
||||
>
|
||||
<text class="switch-label">切换账号</text>
|
||||
<u-icon name="arrow-right" color="#C9CDD4" size="28" />
|
||||
</view>
|
||||
<account-switch-panel
|
||||
ref="accountSwitchPanel"
|
||||
mode="switch"
|
||||
@switched="onSwitched"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccountSwitchPanel from '@/components/account-switch/account-switch-panel.vue';
|
||||
import { getDoctorWxSiblingAccounts } from '@/api/doctorAuth.js';
|
||||
import {
|
||||
saveLoginSession,
|
||||
fetchProfileAfterLogin,
|
||||
navigateAfterLogin,
|
||||
} from '@/utils/loginSession.js';
|
||||
|
||||
export default {
|
||||
name: 'AccountSwitchEntry',
|
||||
components: { AccountSwitchPanel },
|
||||
data() {
|
||||
return {
|
||||
canSwitch: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.checkAccounts();
|
||||
},
|
||||
methods: {
|
||||
checkAccounts() {
|
||||
getDoctorWxSiblingAccounts()
|
||||
.then((wrap) => {
|
||||
if (wrap && wrap.ok) {
|
||||
this.canSwitch = (wrap.data || []).length > 1;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.canSwitch = false;
|
||||
});
|
||||
},
|
||||
openSwitch() {
|
||||
this.$refs.accountSwitchPanel.open();
|
||||
},
|
||||
async onSwitched(result) {
|
||||
saveLoginSession(result);
|
||||
await fetchProfileAfterLogin(result);
|
||||
this.$toast('切换成功');
|
||||
setTimeout(() => {
|
||||
navigateAfterLogin(result);
|
||||
}, 400);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 36rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
}
|
||||
|
||||
.switch-row-hover {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 30rpx;
|
||||
color: #31353d;
|
||||
}
|
||||
</style>
|
||||
275
components/account-switch/account-switch-panel.vue
Normal file
275
components/account-switch/account-switch-panel.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="mode === 'switch'"
|
||||
:show="visible"
|
||||
:overlay="false"
|
||||
@leave="handlePageLeave"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<u-popup
|
||||
v-model="visible"
|
||||
mode="bottom"
|
||||
length="80%"
|
||||
border-radius="24"
|
||||
z-index="10078"
|
||||
:closeable="true"
|
||||
:mask-close-able="true"
|
||||
:safe-area-inset-bottom="true"
|
||||
@close="handleCancel"
|
||||
>
|
||||
<view class="sheet-inner white">
|
||||
<view class="sheet-title">{{ mode === 'switch' ? '切换账号' : '选择登录账号' }}</view>
|
||||
<scroll-view scroll-y class="account-scroll">
|
||||
<view
|
||||
v-for="(item, index) in accounts"
|
||||
:key="item._rowKey"
|
||||
class="account-item"
|
||||
:class="{ active: isSelected(item) }"
|
||||
hover-class="account-item-hover"
|
||||
@tap.stop="selectAccount(item)"
|
||||
>
|
||||
<view class="name">{{ item.nick_name }}</view>
|
||||
<view class="meta">
|
||||
{{ item.role_name }}<text v-if="item.store_name"> · {{ item.store_name }}</text><text v-if="item.login_account"> · {{ item.login_account }}</text>
|
||||
</view>
|
||||
<view v-if="item.is_current" class="current-tag">当前</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="actions">
|
||||
<button class="btn cancel" @tap.stop="handleCancel">取消</button>
|
||||
<button class="btn confirm" :loading="loading" @tap.stop="handleConfirm">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { switchDoctorWxAccount, getDoctorWxSiblingAccounts } from '@/api/doctorAuth.js';
|
||||
|
||||
export default {
|
||||
name: 'AccountSwitchPanel',
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'login',
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
accounts: [],
|
||||
selected: null,
|
||||
loading: false,
|
||||
defaultAccountId: null,
|
||||
defaultAccountType: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
normalizeAccounts(accounts) {
|
||||
return (accounts || []).map((item, index) => ({
|
||||
...item,
|
||||
_rowKey: `${item.account_type}_${item.id}_${index}`,
|
||||
}));
|
||||
},
|
||||
resolveDefaultSelection(accounts, options = {}) {
|
||||
const list = accounts || [];
|
||||
if (!list.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.mode === 'switch') {
|
||||
return list.find((item) => item.is_current) || list[0];
|
||||
}
|
||||
|
||||
const defaultId = options.defaultAccountId ?? this.defaultAccountId;
|
||||
const defaultType = options.defaultAccountType ?? this.defaultAccountType;
|
||||
if (defaultId != null && defaultType) {
|
||||
const matched = list.find(
|
||||
(item) => item.account_type === defaultType && Number(item.id) === Number(defaultId)
|
||||
);
|
||||
if (matched) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
|
||||
return list.find((item) => item.password_matched) || list[0];
|
||||
},
|
||||
showPanel() {
|
||||
this.$nextTick(() => {
|
||||
this.visible = true;
|
||||
});
|
||||
},
|
||||
open(accounts = null, options = {}) {
|
||||
this.defaultAccountId = options.defaultAccountId ?? null;
|
||||
this.defaultAccountType = options.defaultAccountType ?? null;
|
||||
|
||||
if (Array.isArray(accounts)) {
|
||||
this.accounts = this.normalizeAccounts(accounts);
|
||||
this.selected = this.resolveDefaultSelection(this.accounts, options);
|
||||
this.showPanel();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return getDoctorWxSiblingAccounts().then((wrap) => {
|
||||
if (!wrap || !wrap.ok) {
|
||||
return Promise.reject(new Error('获取账号列表失败'));
|
||||
}
|
||||
this.accounts = this.normalizeAccounts(wrap.data || []);
|
||||
this.selected = this.resolveDefaultSelection(this.accounts, options);
|
||||
this.showPanel();
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
},
|
||||
handlePageLeave() {
|
||||
this.close();
|
||||
},
|
||||
isSelected(item) {
|
||||
return this.selected && this.selected._rowKey === item._rowKey;
|
||||
},
|
||||
selectAccount(item) {
|
||||
this.selected = item;
|
||||
},
|
||||
handleCancel() {
|
||||
this.close();
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleConfirm() {
|
||||
if (!this.selected) {
|
||||
this.$toast('请选择账号');
|
||||
return;
|
||||
}
|
||||
if (this.mode === 'switch') {
|
||||
if (this.selected.is_current) {
|
||||
this.$toast('已是当前账号');
|
||||
return;
|
||||
}
|
||||
this.submitSwitch();
|
||||
return;
|
||||
}
|
||||
this.$emit('confirm', this.selected);
|
||||
this.close();
|
||||
},
|
||||
resolveSwitchError(wrap, err) {
|
||||
return wrap?.res?.msg
|
||||
|| wrap?.res?.message
|
||||
|| err?.msg
|
||||
|| err?.message
|
||||
|| '切换失败';
|
||||
},
|
||||
submitSwitch() {
|
||||
this.loading = true;
|
||||
switchDoctorWxAccount({
|
||||
account_type: this.selected.account_type,
|
||||
account_id: this.selected.id,
|
||||
}).then((wrap) => {
|
||||
if (!wrap || !wrap.ok || !wrap.data || !wrap.data.token) {
|
||||
return Promise.reject({
|
||||
message: this.resolveSwitchError(wrap),
|
||||
wrap,
|
||||
});
|
||||
}
|
||||
this.$emit('switched', wrap.data);
|
||||
this.close();
|
||||
}).catch((err) => {
|
||||
const message = this.resolveSwitchError(err?.wrap, err);
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
});
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sheet-inner {
|
||||
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24rpx;
|
||||
flex-shrink: 0;
|
||||
color: #222b2a;
|
||||
}
|
||||
|
||||
.account-scroll {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.account-item {
|
||||
position: relative;
|
||||
padding: 24rpx;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.account-item.active {
|
||||
border-color: #6acdbb;
|
||||
background: #f0fbf8;
|
||||
}
|
||||
|
||||
.account-item-hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
padding-right: 80rpx;
|
||||
color: #222b2a;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.current-tag {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-top: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.confirm {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
284
components/doctor-login/doctor-login.vue
Normal file
284
components/doctor-login/doctor-login.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<view class="container safe-area-inset-bottom">
|
||||
<view class="title">欢迎登录</view>
|
||||
<view class="box">
|
||||
<view class="info">
|
||||
<view class="pwd">
|
||||
<d-input
|
||||
:maxlength="11"
|
||||
height="96"
|
||||
v-model="form.mobile"
|
||||
:prefixIcon="require('../../static/image/act.png')"
|
||||
borderRadius="96rpx"
|
||||
prefixIconSize="40rpx"
|
||||
placeholder="请输入手机号"
|
||||
:placeholder-style="{ fontSize: '32rpx' }"
|
||||
:custom-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="pwd">
|
||||
<d-input
|
||||
height="96"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
:prefixIcon="require('../../static/image/pwd.png')"
|
||||
password
|
||||
borderRadius="96rpx"
|
||||
prefixIconSize="40rpx"
|
||||
placeholder="请输入登录密码"
|
||||
:placeholder-style="{ fontSize: '32rpx' }"
|
||||
:custom-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="pwd">
|
||||
<u-field v-model="smsCode"
|
||||
placeholder="请填写验证码"
|
||||
:prefixIcon="require('../../static/image/pwd.png')"
|
||||
:placeholder-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }"
|
||||
:custom-style="{ fontSize: '32rpx', paddingLeft: '60rpx' }"
|
||||
>
|
||||
<template slot="icon">
|
||||
<!-- <image src="../../../../static/image/pwd.png"></image>-->
|
||||
<u-icon
|
||||
size="40"
|
||||
name="chat"
|
||||
style="margin-left: 15rpx"
|
||||
@click="form.check = !form.check"
|
||||
></u-icon>
|
||||
</template>
|
||||
<u-button size="mini" slot="right" @click="getCode">{{ codeText }}</u-button>
|
||||
</u-field>
|
||||
<u-verification-code ref="uCode" @change="codeChange" class="code"></u-verification-code>
|
||||
</view>
|
||||
|
||||
<view class="flex-row" style="margin-top: 88rpx;">
|
||||
<u-icon
|
||||
size="35"
|
||||
:color="form.check ? '#6ACDBB' : '#e0fdff'"
|
||||
name="checkmark-circle-fill"
|
||||
@click="form.check = !form.check"
|
||||
></u-icon>
|
||||
<view class="agreement">
|
||||
我已阅读并同意
|
||||
<text @click="$go('/subPackages/sub_agreement/agreement?type=service_user_agreement')">《用户服务协议》</text>
|
||||
和
|
||||
<text @click="$go('/subPackages/sub_agreement/agreement?type=service_privacy_agreement')">《隐私权政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn">
|
||||
<u-button :throttle-time="20" shape="circle" @click="submitLogin" :ripple="true" :loading="loading">
|
||||
登录
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sendDoctorWxCode, doctorWxLogin } from '@/api/doctorAuth.js';
|
||||
import {
|
||||
saveLoginSession,
|
||||
fetchProfileAfterLogin,
|
||||
navigateAfterLogin,
|
||||
} from '@/utils/loginSession.js';
|
||||
|
||||
export default {
|
||||
name: 'DoctorLogin',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
mobile: '',
|
||||
password: '',
|
||||
check: false,
|
||||
},
|
||||
smsCode: '',
|
||||
codeText: '',
|
||||
loading: false,
|
||||
pendingLoginPayload: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
codeChange(text) {
|
||||
this.codeText = text;
|
||||
},
|
||||
getCode() {
|
||||
if (!this.$refs.uCode.canGetCode) {
|
||||
this.$u.toast('倒计时结束后再发送');
|
||||
return;
|
||||
}
|
||||
if (!this.form.mobile) {
|
||||
this.$toast('请输入手机号');
|
||||
return;
|
||||
}
|
||||
if (!this.form.password) {
|
||||
this.$toast('请输入密码');
|
||||
return;
|
||||
}
|
||||
uni.showLoading({ title: '正在获取验证码' });
|
||||
sendDoctorWxCode(this.form.mobile, this.form.password)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$refs.uCode.start();
|
||||
if (res.result && res.result.code) {
|
||||
this.smsCode = res.result.code;
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
uni.hideLoading();
|
||||
});
|
||||
},
|
||||
submitLogin() {
|
||||
if (!this.form.mobile) {
|
||||
this.$toast('请输入手机号');
|
||||
return;
|
||||
}
|
||||
if (!this.smsCode) {
|
||||
this.$toast('请输入验证码');
|
||||
return;
|
||||
}
|
||||
if (!this.form.check) {
|
||||
this.$toast('请勾选协议');
|
||||
return;
|
||||
}
|
||||
this.doLogin({});
|
||||
},
|
||||
doLogin(extra = {}) {
|
||||
this.loading = true;
|
||||
const payload = {
|
||||
phone: this.form.mobile,
|
||||
password: this.form.password,
|
||||
code: this.smsCode,
|
||||
...extra,
|
||||
};
|
||||
doctorWxLogin(payload)
|
||||
.then((res) => {
|
||||
if (res.code !== 0 || !res.result) {
|
||||
return Promise.reject(new Error(res.message || res.msg || '登录失败'));
|
||||
}
|
||||
const result = res.result;
|
||||
if (result.need_select && result.accounts && result.accounts.length) {
|
||||
this.pendingLoginPayload = payload;
|
||||
this.$emit('need-select', {
|
||||
accounts: result.accounts,
|
||||
defaultAccountId: result.default_account_id,
|
||||
defaultAccountType: result.default_account_type,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return this.finishLogin(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$toast(err.message || '登录失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
onAccountPicked(account) {
|
||||
this.doLogin({
|
||||
account_type: account.account_type,
|
||||
account_id: account.id,
|
||||
});
|
||||
},
|
||||
async finishLogin(result) {
|
||||
saveLoginSession(result);
|
||||
await fetchProfileAfterLogin(result);
|
||||
this.$toast('登录成功');
|
||||
if (result.account_type !== 'admin') {
|
||||
uni.$emit('user-login');
|
||||
}
|
||||
setTimeout(() => {
|
||||
navigateAfterLogin(result);
|
||||
}, 400);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 750rpx;
|
||||
padding-top: 200rpx;
|
||||
text-align: center;
|
||||
font-size: 48rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #0D111A;
|
||||
background: linear-gradient(180deg, #EAF2FF 0%, #FFFFFF 100%);
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 34rpx;
|
||||
|
||||
.agreement {
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
margin-left: 16rpx;
|
||||
|
||||
text {
|
||||
color: #2979FF;
|
||||
size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
box-sizing: border-box;
|
||||
padding: 0 52rpx;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 32rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #1777FF;
|
||||
background: #F9FAFB;
|
||||
}
|
||||
|
||||
.pwd {
|
||||
width: 646rpx;
|
||||
height: 96rpx;
|
||||
background: #F9FAFB;
|
||||
border-radius: 48rpx 48rpx 48rpx 48rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #CCCCCC;
|
||||
margin: 0rpx 0 30rpx;
|
||||
|
||||
::v-deep button {
|
||||
color: #1777FF;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
::v-deep button {
|
||||
width: 648rpx;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
background: #6ACDBB;
|
||||
border: 0;
|
||||
font-size: 32rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,473 +1,50 @@
|
||||
<template>
|
||||
<view class="container safe-area-inset-bottom">
|
||||
|
||||
<view class="title">
|
||||
欢迎登录
|
||||
</view>
|
||||
|
||||
<view class="box">
|
||||
<view class="info">
|
||||
<view class="pwd">
|
||||
<d-input :maxlength="11" height="96" v-model="form['mobile']"
|
||||
:prefixIcon="require('../../static/image/act.png')" borderRadius="96rpx" prefixIconSize="40rpx"
|
||||
:placeholder="loginMode === 'clinic_admin' ? '请输入手机号' : '请输入手机号'" @input="is_d=false" :placeholder-style="{fontSize:'32rpx'}"
|
||||
:custom-style="{fontSize:'32rpx',paddingLeft:'60rpx'}" />
|
||||
</view>
|
||||
|
||||
<view class="pwd" v-if="loginMode === 'clinic_admin'">
|
||||
<d-input height="96" v-model="clinicPassword" type="password" password
|
||||
borderRadius="96rpx" placeholder="请输入后台登录密码" :placeholder-style="{fontSize:'32rpx'}"
|
||||
:custom-style="{fontSize:'32rpx',paddingLeft:'32rpx'}" />
|
||||
</view>
|
||||
|
||||
<view class="pwd">
|
||||
<u-field v-model="smsCode" placeholder="请填写验证码" :placeholder-style="{fontSize:'32rpx'}">
|
||||
<u-button size="mini" slot="right" @click="getCode">{{codeText}}</u-button>
|
||||
</u-field>
|
||||
<u-verification-code ref="uCode" @change="codeChange" class="code">
|
||||
</u-verification-code>
|
||||
</view>
|
||||
|
||||
<view class="flex-row" style="margin-top: 88rpx;">
|
||||
<u-icon size="35" :color="form['check']?'#6ACDBB':'#e0fdff'" name="checkmark-circle-fill"
|
||||
@click="form['check']=!form['check']"></u-icon>
|
||||
<view class="agreement">
|
||||
我已阅读并同意
|
||||
<text
|
||||
@click="$go('/subPackages/sub_agreement/agreement?type=service_user_agreement')">《用户服务协议》</text>
|
||||
和
|
||||
<text
|
||||
@click="$go('/subPackages/sub_agreement/agreement?type=service_privacy_agreement')">《隐私权政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- :disabled="!form['mobile']||!form['pwd']" -->
|
||||
<view class="btn">
|
||||
<u-button :throttle-time="20" shape="circle" @click="register" :ripple="true" :loading="loading">
|
||||
登录</u-button>
|
||||
</view>
|
||||
|
||||
<view class="register">
|
||||
<text @click="switchClinicAdmin">{{ loginMode === 'clinic_admin' ? '返回医生/药师登录' : '我是诊所管理员' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- <view class="register">-->
|
||||
<!-- <text @click="$go('../../subPackages/sub_workbench/workbench_identity')">手机号注册</text>-->
|
||||
<!-- </view>-->
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
<view>
|
||||
<doctor-login ref="doctorLogin" @need-select="openAccountPicker" />
|
||||
<account-switch-panel
|
||||
ref="accountSwitchPanel"
|
||||
mode="login"
|
||||
@confirm="onAccountPicked"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
login,
|
||||
register,
|
||||
agreement,
|
||||
getUserInfo,
|
||||
loginOrRe
|
||||
} from '../../api/all';
|
||||
import {
|
||||
sendClinicAdminCode,
|
||||
clinicAdminLogin,
|
||||
getClinicAdminMyInfo,
|
||||
} from '../../api/clinicAdmin';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loginMode: 'service_user',
|
||||
loading: false,
|
||||
form: {
|
||||
check: false
|
||||
},
|
||||
switchVal: false,
|
||||
is_d: false,
|
||||
statusBarHeight: this.$statusBarHeight,
|
||||
navbarHeight: this.$navbarHeight,
|
||||
mobile: '',
|
||||
codeText: '',
|
||||
smsCode: '',
|
||||
clinicPassword: '',
|
||||
code: ''
|
||||
};
|
||||
},
|
||||
onLoad(e) {
|
||||
import DoctorLogin from '@/components/doctor-login/doctor-login.vue';
|
||||
import AccountSwitchPanel from '@/components/account-switch/account-switch-panel.vue';
|
||||
|
||||
// const accountInfo = uni.getAccountInfoSync();
|
||||
// console.log('appid', accountInfo.miniProgram.appId); // 小程序 appId
|
||||
},
|
||||
mounted() {
|
||||
const clinicToken = uni.getStorageSync('clinic_admin_token')
|
||||
if (clinicToken && uni.getStorageSync('loginMode') === 'clinic_admin') {
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/subPackages/sub_clinic_admin/home/index' })
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
const token = uni.getStorageSync('token')
|
||||
const info = uni.getStorageSync('loginInfo')
|
||||
if (token&&info) {
|
||||
// 药师
|
||||
if (info.role == 2 && info.status > 0 && info!= 3) {
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/pharmacist/index', 3)
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
|
||||
// 医生
|
||||
if (info.role == 1 && info.status > 0 && info.status != 3) {
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/workbench/index', 2)
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchClinicAdmin() {
|
||||
this.loginMode = this.loginMode === 'clinic_admin' ? 'service_user' : 'clinic_admin'
|
||||
this.clinicPassword = ''
|
||||
this.smsCode = ''
|
||||
},
|
||||
toLogin() {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (loginRes) => {
|
||||
this.code = loginRes.code
|
||||
}
|
||||
})
|
||||
},
|
||||
//登录或者注册接口
|
||||
register() {
|
||||
if (!uni.$u.test.mobile(this.form['mobile'])) {
|
||||
this.$toast('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
if (!this.smsCode) {
|
||||
this.$toast('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.form['check']) {
|
||||
this.$toast('请勾选协议')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.loginMode === 'clinic_admin') {
|
||||
if (!this.clinicPassword) {
|
||||
this.$toast('请输入后台登录密码')
|
||||
return
|
||||
}
|
||||
this.clinicAdminLoginFlow()
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
store_id: '11001',
|
||||
mobile: this.form['mobile'],
|
||||
status: 3,
|
||||
smsCode: this.smsCode,
|
||||
code: this.code
|
||||
}
|
||||
|
||||
loginOrRe(data).then(res => {
|
||||
if (res.errcode != -1) {
|
||||
this.$toast('登录成功')
|
||||
// this.$toast(res.data[0])
|
||||
uni.setStorageSync('token', res.data['token'])
|
||||
// console.log(res);
|
||||
uni.setStorageSync('identity', res.data['ServiceUser']['role'])
|
||||
uni.setStorageSync('logInfo', res.data['ServiceUser'])
|
||||
// 记录店铺id
|
||||
uni.setStorageSync('store_id', res.data['StoreDoctor']['store_id'])
|
||||
|
||||
// 存储doctor_id或user_id,用于WebSocket绑定
|
||||
if (res.data['ServiceUser'] && res.data['ServiceUser']['id']) {
|
||||
uni.setStorageSync('doctor_id', res.data['ServiceUser']['id']);
|
||||
uni.setStorageSync('user_id', res.data['ServiceUser']['id']);
|
||||
}
|
||||
|
||||
// 触发WebSocket初始化(登录成功后)
|
||||
uni.$emit('user-login');
|
||||
|
||||
const data = {
|
||||
store_id: uni.getStorageSync('store_id') || res.data['StoreDoctor']['store_id'] || '11001',
|
||||
}
|
||||
getUserInfo(data).then(res => {
|
||||
if (res.errcode != -1) {
|
||||
uni.setStorageSync('loginInfo', res.data)
|
||||
uni.setStorageSync('role', res.data.role)
|
||||
uni.setStorageSync('login_id', res.data['id'])
|
||||
uni.setStorageSync('login_status', res.data['status'])
|
||||
}
|
||||
// 未完善信息
|
||||
if (res.data.status == 0 ) {
|
||||
this.$toast('您还没有完善信息,请先完善信息')
|
||||
// this.$go('../../subPackages/sub_workbench/workbench_upInfo/index')
|
||||
uni.navigateTo({
|
||||
url:'../../subPackages/sub_workbench/workbench_upInfo/index'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$toast(res.msg)
|
||||
})
|
||||
|
||||
|
||||
// 药师
|
||||
if (res.data.ServiceUser.role == 2 && res.data.ServiceUser.status ==2 ) {
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/pharmacist/index', 3)
|
||||
}, 600)
|
||||
return
|
||||
}
|
||||
|
||||
// 医生 res.data.ServiceUser.status > 0 && res.data .ServiceUser.status != 3
|
||||
if (res.data.ServiceUser.role == 1 && res.data.ServiceUser.status==2 ) {
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/workbench/index', 2)
|
||||
}, 600)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 医生未通过审核
|
||||
if (res.data.ServiceUser.role == 1 && res.data.ServiceUser.status == 3) {
|
||||
// this.$toast(res.data.ServiceUser.reason)
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/workbench/examine')
|
||||
}, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
// 医生审核中
|
||||
if (res.data.ServiceUser.role == 1 && res.data.ServiceUser.status == 1) {
|
||||
this.$toast('您的账号还在审核中')
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/workbench/examine')
|
||||
}, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
// 药师未通过审核
|
||||
if (res.data.ServiceUser.role == 2 && res.data.ServiceUser.status == 3) {
|
||||
// this.$toast(res.data.ServiceUser.reason)
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/workbench/examine')
|
||||
}, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
// 药师审核中
|
||||
if (res.data.ServiceUser.role == 2 && res.data.ServiceUser.status == 1) {
|
||||
this.$toast('您的账号还在审核中')
|
||||
setTimeout(() => {
|
||||
this.$go('/pages/workbench/examine')
|
||||
}, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (res.errcode == -1) {
|
||||
// this.$toast('您还没有账号,请先注册')
|
||||
// setTimeout(() => {
|
||||
// this.$go('../../subPackages/sub_workbench/workbench_identity')
|
||||
// }, 600)
|
||||
|
||||
this.$toast(res.msg)
|
||||
return
|
||||
}
|
||||
})
|
||||
},
|
||||
checkboxChange(val) {
|
||||
// console.log(val);
|
||||
},
|
||||
codeChange(text) {
|
||||
this.codeText = text;
|
||||
},
|
||||
clinicAdminLoginFlow() {
|
||||
this.loading = true
|
||||
clinicAdminLogin(this.form['mobile'], this.clinicPassword, this.smsCode).then(res => {
|
||||
if (res.code !== 0 || !res.result || !res.result.token) {
|
||||
this.$toast(res.message || res.msg || '登录失败')
|
||||
return Promise.reject()
|
||||
}
|
||||
uni.setStorageSync('loginMode', 'clinic_admin')
|
||||
uni.setStorageSync('clinic_admin_token', res.result.token)
|
||||
return getClinicAdminMyInfo()
|
||||
}).then((wrap) => {
|
||||
if (!wrap || !wrap.ok) return
|
||||
uni.setStorageSync('clinic_admin_user', wrap.data)
|
||||
this.$toast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/subPackages/sub_clinic_admin/home/index' })
|
||||
}, 400)
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
getCode() {
|
||||
if (this.$refs.uCode.canGetCode) {
|
||||
uni.showLoading({ title: '正在获取验证码' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
if (this.loginMode === 'clinic_admin') {
|
||||
if (!this.clinicPassword) {
|
||||
this.$toast('请先输入后台登录密码')
|
||||
return
|
||||
}
|
||||
sendClinicAdminCode(this.form['mobile']).then(res => {
|
||||
if (res.code === 0 || res.errcode != -1) {
|
||||
this.$refs.uCode.start();
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const data = {
|
||||
store_id: '11001',
|
||||
mobile: this.form['mobile']
|
||||
}
|
||||
register(data).then(res => {
|
||||
// console.log(res);
|
||||
if (res.errcode != -1) {
|
||||
// this.code = res.data.code
|
||||
uni.setStorageSync('smsCode', res.data['smsCode'])
|
||||
// if(this.code==res.data.smsCode){
|
||||
// this.$go('../../subPackages/sub_workbench/workbench_upInfo/index')
|
||||
// }else{
|
||||
// this.$u.toast('验证码错误')
|
||||
// return
|
||||
// }
|
||||
}
|
||||
})
|
||||
this.$refs.uCode.start();
|
||||
}, 1000);
|
||||
} else {
|
||||
this.$u.toast('倒计时结束后再发送');
|
||||
}
|
||||
export default {
|
||||
components: { DoctorLogin, AccountSwitchPanel },
|
||||
onShow() {
|
||||
const clinicToken = uni.getStorageSync('clinic_admin_token');
|
||||
const token = uni.getStorageSync('token');
|
||||
const loginMode = uni.getStorageSync('loginMode');
|
||||
if (loginMode === 'clinic_admin' && clinicToken) {
|
||||
uni.reLaunch({ url: '/subPackages/sub_clinic_admin/home/index' });
|
||||
return;
|
||||
}
|
||||
if (token) {
|
||||
const role = Number(uni.getStorageSync('identity') || uni.getStorageSync('role') || 0);
|
||||
if (role === 2) {
|
||||
uni.reLaunch({ url: '/pages/pharmacist/index' });
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/workbench/index' });
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openAccountPicker({ accounts, defaultAccountId, defaultAccountType }) {
|
||||
this.$refs.accountSwitchPanel.open(accounts, {
|
||||
defaultAccountId,
|
||||
defaultAccountType,
|
||||
});
|
||||
},
|
||||
onAccountPicked(account) {
|
||||
this.$refs.doctorLogin.onAccountPicked(account);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 750rpx;
|
||||
padding-top: 200rpx;
|
||||
text-align: center;
|
||||
font-size: 48rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #0D111A;
|
||||
background: linear-gradient(180deg, #EAF2FF 0%, #FFFFFF 100%);
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 34rpx;
|
||||
|
||||
.agreement {
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
margin-left: 16rpx;
|
||||
|
||||
text {
|
||||
color: #2979FF;
|
||||
size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
box-sizing: border-box;
|
||||
padding: 0 52rpx;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 32rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #1777FF;
|
||||
background: #F9FAFB;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
font-size: 24rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.pwd {
|
||||
width: 646rpx;
|
||||
height: 96rpx;
|
||||
background: #F9FAFB;
|
||||
border-radius: 48rpx 48rpx 48rpx 48rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #CCCCCC;
|
||||
margin: 0rpx 0 30rpx;
|
||||
|
||||
::v-deep button {
|
||||
color: #1777FF;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 80rpx;
|
||||
font-size: 28rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #6C7380;
|
||||
}
|
||||
|
||||
.btn {
|
||||
::v-deep button {
|
||||
width: 648rpx;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
background: #6ACDBB;
|
||||
border: 0;
|
||||
font-size: 32rpx;
|
||||
font-family: PingFang SC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.register {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
|
||||
text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
word-spacing: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
export default {
|
||||
components: {
|
||||
myService,
|
||||
myPharmacist
|
||||
myPharmacist,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
</view>
|
||||
<view class="box" :style="{marginTop: statusBarHeight+navbarHeight + 'px'}">
|
||||
<view class="store_" @click="openShow=true">
|
||||
{{doctorInfo.store.store.name || '默认门店'}}
|
||||
<u-icon name="map-fill" color="#fff" size="28" margin-left="12"></u-icon>
|
||||
</view>
|
||||
{{doctorInfo.store.store.name || '默认门店'}}
|
||||
<u-icon name="map-fill" color="#fff" size="28" margin-left="12"></u-icon>
|
||||
</view>
|
||||
<view class="box_info">
|
||||
<view class="box_info_left">
|
||||
|
||||
@@ -885,6 +885,7 @@ page {
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
padding: 0 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.u-icon {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -28,6 +28,7 @@
|
||||
|
||||
<!-- 操作面板(通栏) -->
|
||||
<view class="setting-panel">
|
||||
<account-switch-entry />
|
||||
<view
|
||||
class="setting-item action-item"
|
||||
hover-class="item-hover"
|
||||
@@ -46,9 +47,10 @@
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
components: { ClinicPageLayout, AccountSwitchEntry },
|
||||
data() {
|
||||
return {
|
||||
displayName: '',
|
||||
|
||||
@@ -1,135 +1,316 @@
|
||||
<template>
|
||||
<clinic-page-layout title="药品详情" :show-back="true">
|
||||
<view v-if="info.id" class="card">
|
||||
<image class="hero" :src="display.imageUrl" mode="aspectFit" />
|
||||
<view class="row"><text>药品名称</text><text>{{ display.drugName }}</text></view>
|
||||
<view class="row"><text>拼音</text><text>{{ display.pinyin }}</text></view>
|
||||
<view class="row"><text>供货价</text><text>{{ display.buyPrice }}</text></view>
|
||||
<view class="row"><text>建议售价</text><text>{{ display.suggestPrice }}</text></view>
|
||||
<view class="row"><text>售价</text><text>{{ display.price }}</text></view>
|
||||
<view class="row"><text>库存</text><text>{{ display.stock }}</text></view>
|
||||
<view class="row"><text>状态</text><text>{{ display.statusText }}</text></view>
|
||||
</view>
|
||||
<clinic-page-layout title="药品详情" :show-back="true">
|
||||
<view v-if="info.id" class="page-container">
|
||||
<!-- 头部信息卡片 -->
|
||||
<view class="card hero-card">
|
||||
<view class="image-wrapper">
|
||||
<image class="hero" :src="display.imageUrl" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="main-info">
|
||||
<text class="drug-name">{{ display.drugName }}</text>
|
||||
<text class="pinyin">拼音: {{ display.pinyin_simple || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="info.id" class="btn-group">
|
||||
<button class="btn secondary" @click="openPriceEdit">修改售价</button>
|
||||
<button class="btn primary" @click="toggleStatus">
|
||||
{{ display.isOnShelf ? '下架' : '上架' }}
|
||||
</button>
|
||||
</view>
|
||||
<!-- 核心数据卡片 -->
|
||||
<view class="card data-card">
|
||||
<view class="row">
|
||||
<text class="label">状态</text>
|
||||
<view class="status-tag" :class="display.isOnShelf ? 'tag-on' : 'tag-off'">
|
||||
{{ display.statusText }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">库存</text>
|
||||
<text class="value stock-value">{{ display.stock }}</text>
|
||||
</view>
|
||||
|
||||
<price-edit-modal
|
||||
:visible="priceModalVisible"
|
||||
:item="info.id ? info : null"
|
||||
@close="closePriceEdit"
|
||||
@confirm="onPriceConfirm"
|
||||
/>
|
||||
</clinic-page-layout>
|
||||
<view class="divider"></view>
|
||||
|
||||
<view class="row">
|
||||
<text class="label">供货价</text>
|
||||
<text class="value">{{ display.buyPrice }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">建议售价</text>
|
||||
<text class="value">{{ display.suggestPrice }}</text>
|
||||
</view>
|
||||
<view class="row highlight-row">
|
||||
<text class="label">当前售价</text>
|
||||
<text class="value price-large">{{ display.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮组 -->
|
||||
<view v-if="info.id" class="action-group">
|
||||
<view class="btn-large btn-outline" hover-class="btn-hover-opacity" @click="openPriceEdit">
|
||||
修改售价
|
||||
</view>
|
||||
<view
|
||||
class="btn-large"
|
||||
:class="display.isOnShelf ? 'btn-danger' : 'btn-primary'"
|
||||
hover-class="btn-hover-opacity"
|
||||
@click="toggleStatus"
|
||||
>
|
||||
{{ display.isOnShelf ? '下架该药品' : '上架该药品' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<price-edit-modal
|
||||
:visible="priceModalVisible"
|
||||
:item="info.id ? info : null"
|
||||
@close="closePriceEdit"
|
||||
@confirm="onPriceConfirm"
|
||||
/>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PriceEditModal from './components/PriceEditModal.vue'
|
||||
import {
|
||||
getWarehouseDetail,
|
||||
updateWarehouseStatus,
|
||||
updateWarehousePrice,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import { mapWarehouseRow } from '../common/display.js'
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PriceEditModal from './components/PriceEditModal.vue'
|
||||
import {
|
||||
getWarehouseDetail,
|
||||
updateWarehouseStatus,
|
||||
updateWarehousePrice,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import { mapWarehouseRow } from '../common/display.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, PriceEditModal },
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
info: {},
|
||||
priceModalVisible: false,
|
||||
priceSubmitting: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
display() {
|
||||
return mapWarehouseRow(this.info)
|
||||
},
|
||||
},
|
||||
onLoad(q) {
|
||||
this.id = Number(q.id || 0)
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getWarehouseDetail(this.id).then(w => {
|
||||
if (w.ok) this.info = w.data || {}
|
||||
})
|
||||
},
|
||||
openPriceEdit() {
|
||||
this.priceModalVisible = true
|
||||
},
|
||||
closePriceEdit() {
|
||||
this.priceModalVisible = false
|
||||
},
|
||||
onPriceConfirm(price) {
|
||||
if (this.priceSubmitting) return
|
||||
this.priceSubmitting = true
|
||||
updateWarehousePrice({ id: this.id, price }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '修改成功' })
|
||||
this.closePriceEdit()
|
||||
this.load()
|
||||
}
|
||||
}).finally(() => { this.priceSubmitting = false })
|
||||
},
|
||||
toggleStatus() {
|
||||
updateWarehouseStatus(this.id).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '操作成功' })
|
||||
this.load()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
export default {
|
||||
components: { ClinicPageLayout, PriceEditModal },
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
info: {},
|
||||
priceModalVisible: false,
|
||||
priceSubmitting: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
display() {
|
||||
return mapWarehouseRow(this.info)
|
||||
},
|
||||
},
|
||||
onLoad(q) {
|
||||
this.id = Number(q.id || 0)
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getWarehouseDetail(this.id).then(w => {
|
||||
if (w.ok) this.info = w.data || {}
|
||||
})
|
||||
},
|
||||
openPriceEdit() {
|
||||
this.priceModalVisible = true
|
||||
},
|
||||
closePriceEdit() {
|
||||
this.priceModalVisible = false
|
||||
},
|
||||
onPriceConfirm(price) {
|
||||
if (this.priceSubmitting) return
|
||||
this.priceSubmitting = true
|
||||
updateWarehousePrice({ id: this.id, price }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '修改成功' })
|
||||
this.closePriceEdit()
|
||||
this.load()
|
||||
}
|
||||
}).finally(() => { this.priceSubmitting = false })
|
||||
},
|
||||
toggleStatus() {
|
||||
updateWarehouseStatus(this.id).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '操作成功' })
|
||||
this.load()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 同步了列表页的核心色板 */
|
||||
$theme-color: #6acdbb;
|
||||
$theme-color-light: rgba(106, 205, 187, 0.12);
|
||||
$text-primary: #2c3e50;
|
||||
$text-secondary: #8a97a3;
|
||||
$text-light: #b0b8c1;
|
||||
$bg-color: #f7f9fa;
|
||||
$border-color: #ebedf0;
|
||||
$danger-color: #ff6b6b;
|
||||
$danger-color-light: rgba(255, 107, 107, 0.1);
|
||||
|
||||
.page-container {
|
||||
padding: 24rpx;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
/* 头部卡片特异样式 */
|
||||
.hero-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 40rpx;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
background: $bg-color;
|
||||
border: 1rpx solid $border-color;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.hero {
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f8f8f8;
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.main-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drug-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.pinyin {
|
||||
font-size: 26rpx;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
/* 核心数据卡片样式 */
|
||||
.data-card {
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
font-size: 28rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
|
||||
.divider {
|
||||
height: 1rpx;
|
||||
background-color: $border-color;
|
||||
margin: 8rpx 0;
|
||||
}
|
||||
.btn {
|
||||
border-radius: 44rpx;
|
||||
font-size: 30rpx;
|
||||
margin: 0;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: $text-secondary;
|
||||
}
|
||||
.btn.primary {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: $text-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn.secondary {
|
||||
background: #fff;
|
||||
color: #6acdbb;
|
||||
border: 1rpx solid #6acdbb;
|
||||
|
||||
.stock-value {
|
||||
font-family: monospace; /* 数字更清晰 */
|
||||
font-size: 30rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
.highlight-row {
|
||||
margin-top: 8rpx;
|
||||
background: #fafbfa; /* 极浅的背景强调 */
|
||||
margin: 0 -32rpx;
|
||||
padding: 32rpx;
|
||||
border-bottom-left-radius: 24rpx;
|
||||
border-bottom-right-radius: 24rpx;
|
||||
}
|
||||
|
||||
.highlight-row .label {
|
||||
color: $text-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.price-large {
|
||||
font-size: 40rpx;
|
||||
color: $theme-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.status-tag {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.tag-on {
|
||||
color: $theme-color;
|
||||
background: $theme-color-light;
|
||||
}
|
||||
|
||||
.tag-off {
|
||||
color: $text-secondary;
|
||||
background: $bg-color;
|
||||
}
|
||||
|
||||
/* 底部操作按钮组 */
|
||||
.action-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 48rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: $theme-color;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx $theme-color-light;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: $danger-color;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx $danger-color-light;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: #ffffff;
|
||||
color: $theme-color;
|
||||
border: 2rpx solid $theme-color;
|
||||
}
|
||||
|
||||
.btn-hover-opacity {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -1,231 +1,383 @@
|
||||
<template>
|
||||
<clinic-page-layout :title="pageTitle" :show-back="true">
|
||||
<view class="search">
|
||||
<input v-model="keyword" placeholder="搜索药品名称" @confirm="reload" />
|
||||
<text @click="reload">搜索</text>
|
||||
</view>
|
||||
<clinic-page-layout :title="pageTitle" :show-back="true">
|
||||
<!-- 搜索栏优化:胶囊式设计,增加视觉呼吸感 -->
|
||||
<view class="search-wrapper">
|
||||
<view class="search-bar">
|
||||
<input class="search-input" v-model="keyword" placeholder="搜索药品名称" placeholder-class="search-placeholder" @confirm="reload" />
|
||||
<view class="search-btn" hover-class="btn-hover" @click="reload">搜索</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item._wxKey"
|
||||
class="item"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<image class="thumb" :src="rowDisplay(item).imageUrl" mode="aspectFill" />
|
||||
<view class="body">
|
||||
<view class="title">{{ rowDisplay(item).drugName }}</view>
|
||||
<view class="sub">供货 {{ rowDisplay(item).buyPrice }} · 售价 {{ rowDisplay(item).price }}</view>
|
||||
<view class="sub">库存 {{ rowDisplay(item).stock }}</view>
|
||||
</view>
|
||||
<view class="actions" @click.stop>
|
||||
<view class="tag" :class="rowDisplay(item).isOnShelf ? 'on' : 'off'">{{ rowDisplay(item).statusText }}</view>
|
||||
<view class="btn-row">
|
||||
<text class="act-btn" @click="openPriceEdit(item)">改价</text>
|
||||
<text class="act-btn shelf" @click="toggleStatus(item)">
|
||||
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-container">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item._wxKey"
|
||||
class="item-card"
|
||||
hover-class="card-hover"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<!-- 左侧缩略图 -->
|
||||
<image class="thumb" :src="rowDisplay(item).imageUrl" mode="aspectFill" />
|
||||
|
||||
<view v-if="!loading && !list.length" class="empty">暂无药品</view>
|
||||
<!-- 右侧内容区 -->
|
||||
<view class="content">
|
||||
<!-- 顶部:标题与状态标签对齐 -->
|
||||
<view class="header-row">
|
||||
<text class="title">{{ rowDisplay(item).drugName }}</text>
|
||||
<view class="status-tag" :class="rowDisplay(item).isOnShelf ? 'tag-on' : 'tag-off'">
|
||||
{{ rowDisplay(item).statusText }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<price-edit-modal
|
||||
:visible="priceModalVisible"
|
||||
:item="priceEditItem"
|
||||
@close="closePriceEdit"
|
||||
@confirm="onPriceConfirm"
|
||||
/>
|
||||
</clinic-page-layout>
|
||||
<!-- 中部:详细信息 -->
|
||||
<view class="info-group">
|
||||
<text class="info-text">供货 {{ rowDisplay(item).buyPrice }} · 售价 {{ rowDisplay(item).price }}</text>
|
||||
<text class="info-text">库存 {{ rowDisplay(item).stock }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部:操作按钮横向排列,增加点击区域与反馈 -->
|
||||
<view class="action-row" @click.stop>
|
||||
<view class="action-btn outline" hover-class="btn-hover-opacity" @click.stop="openPriceEdit(item)">
|
||||
改价
|
||||
</view>
|
||||
<view class="action-btn" :class="rowDisplay(item).isOnShelf ? 'btn-danger' : 'btn-primary'" hover-class="btn-hover-opacity" @click.stop="toggleStatus(item)">
|
||||
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态优化 -->
|
||||
<view v-if="!loading && !list.length" class="empty-state">
|
||||
<view class="empty-icon"></view>
|
||||
<text class="empty-text">暂无药品数据</text>
|
||||
</view>
|
||||
|
||||
<price-edit-modal
|
||||
:visible="priceModalVisible"
|
||||
:item="priceEditItem"
|
||||
@close="closePriceEdit"
|
||||
@confirm="onPriceConfirm"
|
||||
/>
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PriceEditModal from './components/PriceEditModal.vue'
|
||||
import {
|
||||
getWarehouseList,
|
||||
updateWarehouseStatus,
|
||||
updateWarehousePrice,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import { mapWarehouseRow } from '../common/display.js'
|
||||
import ClinicPageLayout from '../components/clinic-page-layout.vue'
|
||||
import PriceEditModal from './components/PriceEditModal.vue'
|
||||
import {
|
||||
getWarehouseList,
|
||||
updateWarehouseStatus,
|
||||
updateWarehousePrice,
|
||||
} from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import { mapWarehouseRow } from '../common/display.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout, PriceEditModal },
|
||||
data() {
|
||||
return {
|
||||
pageTitle: '仓库管理',
|
||||
warehouseType: '',
|
||||
keyword: '',
|
||||
list: [],
|
||||
page: 1,
|
||||
loading: false,
|
||||
priceModalVisible: false,
|
||||
priceEditItem: null,
|
||||
priceSubmitting: false,
|
||||
}
|
||||
},
|
||||
onLoad(q) {
|
||||
if (q.label) {
|
||||
this.pageTitle = decodeURIComponent(q.label)
|
||||
}
|
||||
if (q.type != null && q.type !== '') {
|
||||
this.warehouseType = q.type
|
||||
}
|
||||
this.reload()
|
||||
},
|
||||
onReachBottom() {
|
||||
this.load(this.page + 1, true)
|
||||
},
|
||||
methods: {
|
||||
rowDisplay(item) {
|
||||
return mapWarehouseRow(item)
|
||||
},
|
||||
reload() {
|
||||
this.page = 1
|
||||
this.load(1, false)
|
||||
},
|
||||
load(page, append) {
|
||||
this.loading = true
|
||||
const params = { page, pageSize: 20 }
|
||||
if (this.keyword) params.name = this.keyword
|
||||
if (this.warehouseType !== '' && this.warehouseType != null) {
|
||||
params.type = this.warehouseType
|
||||
}
|
||||
getWarehouseList(params).then(w => {
|
||||
const items = withWxKey((w.data && w.data.items) || [], 'id', 'wh')
|
||||
this.list = append ? this.list.concat(items) : items
|
||||
this.page = page
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
goDetail(item) {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/warehouse/detail?id=' + item.id })
|
||||
},
|
||||
openPriceEdit(item) {
|
||||
this.priceEditItem = item
|
||||
this.priceModalVisible = true
|
||||
},
|
||||
closePriceEdit() {
|
||||
this.priceModalVisible = false
|
||||
this.priceEditItem = null
|
||||
},
|
||||
onPriceConfirm(price) {
|
||||
if (!this.priceEditItem || this.priceSubmitting) return
|
||||
this.priceSubmitting = true
|
||||
updateWarehousePrice({ id: this.priceEditItem.id, price }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '修改成功' })
|
||||
this.closePriceEdit()
|
||||
this.reload()
|
||||
}
|
||||
}).finally(() => { this.priceSubmitting = false })
|
||||
},
|
||||
toggleStatus(item) {
|
||||
const action = item.status === 2 ? '下架' : '上架'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定' + action + '「' + ((item.drug && item.drug.drug_name) || '') + '」?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
updateWarehouseStatus(item.id).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '操作成功' })
|
||||
item.status = item.status === 2 ? 1 : 2
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
export default {
|
||||
components: { ClinicPageLayout, PriceEditModal },
|
||||
data() {
|
||||
return {
|
||||
pageTitle: '仓库管理',
|
||||
warehouseType: '',
|
||||
keyword: '',
|
||||
list: [],
|
||||
page: 1,
|
||||
loading: false,
|
||||
priceModalVisible: false,
|
||||
priceEditItem: null,
|
||||
priceSubmitting: false,
|
||||
}
|
||||
},
|
||||
onLoad(q) {
|
||||
if (q.label) {
|
||||
this.pageTitle = decodeURIComponent(q.label)
|
||||
}
|
||||
if (q.type != null && q.type !== '') {
|
||||
this.warehouseType = q.type
|
||||
}
|
||||
this.reload()
|
||||
},
|
||||
onReachBottom() {
|
||||
this.load(this.page + 1, true)
|
||||
},
|
||||
methods: {
|
||||
rowDisplay(item) {
|
||||
return mapWarehouseRow(item)
|
||||
},
|
||||
reload() {
|
||||
this.page = 1
|
||||
this.load(1, false)
|
||||
},
|
||||
load(page, append) {
|
||||
this.loading = true
|
||||
const params = { page, pageSize: 20 }
|
||||
if (this.keyword) params.name = this.keyword
|
||||
if (this.warehouseType !== '' && this.warehouseType != null) {
|
||||
params.type = this.warehouseType
|
||||
}
|
||||
getWarehouseList(params).then(w => {
|
||||
const items = withWxKey((w.data && w.data.items) || [], 'id', 'wh')
|
||||
this.list = append ? this.list.concat(items) : items
|
||||
this.page = page
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
goDetail(item) {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/warehouse/detail?id=' + item.id })
|
||||
},
|
||||
openPriceEdit(item) {
|
||||
this.priceEditItem = item
|
||||
this.priceModalVisible = true
|
||||
},
|
||||
closePriceEdit() {
|
||||
this.priceModalVisible = false
|
||||
this.priceEditItem = null
|
||||
},
|
||||
onPriceConfirm(price) {
|
||||
if (!this.priceEditItem || this.priceSubmitting) return
|
||||
this.priceSubmitting = true
|
||||
updateWarehousePrice({ id: this.priceEditItem.id, price }).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '修改成功' })
|
||||
this.closePriceEdit()
|
||||
this.reload()
|
||||
}
|
||||
}).finally(() => { this.priceSubmitting = false })
|
||||
},
|
||||
toggleStatus(item) {
|
||||
const action = item.status === 2 ? '下架' : '上架'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定' + action + '「' + ((item.drug && item.drug.drug_name) || '') + '」?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
updateWarehouseStatus(item.id).then(w => {
|
||||
if (w.ok) {
|
||||
uni.showToast({ title: '操作成功' })
|
||||
item.status = item.status === 2 ? 1 : 2
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
align-items: center;
|
||||
/* 核心主题色变量提取,方便后续统一管理 */
|
||||
$theme-color: #6acdbb;
|
||||
$theme-color-light: rgba(106, 205, 187, 0.12);
|
||||
$text-primary: #2c3e50;
|
||||
$text-secondary: #8a97a3;
|
||||
$text-light: #b0b8c1;
|
||||
$bg-color: #f7f9fa;
|
||||
$border-color: #ebedf0;
|
||||
$danger-color: #ff6b6b;
|
||||
$danger-color-light: rgba(255, 107, 107, 0.1);
|
||||
|
||||
.search-wrapper {
|
||||
padding: 16rpx 24rpx;
|
||||
background-color: transparent;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.search input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
padding: 12rpx 16rpx 12rpx 32rpx;
|
||||
border-radius: 40rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.search text {
|
||||
color: #6acdbb;
|
||||
font-size: 28rpx;
|
||||
margin-left: 16rpx;
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: $text-primary;
|
||||
height: 60rpx;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||||
|
||||
.search-placeholder {
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: $theme-color;
|
||||
color: #ffffff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
padding: 12rpx 36rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-left: 16rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transform: scale(0.99);
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 12rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
background: $bg-color;
|
||||
border: 1rpx solid $border-color;
|
||||
}
|
||||
.body {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
min-width: 0;
|
||||
padding-right: 12rpx;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-size: 30rpx;
|
||||
color: $text-primary;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
/* 单行截断 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sub {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
|
||||
.status-tag {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12rpx;
|
||||
|
||||
.tag-on {
|
||||
color: $theme-color;
|
||||
background: $theme-color-light;
|
||||
}
|
||||
.tag {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.tag-off {
|
||||
color: $text-secondary;
|
||||
background: $bg-color;
|
||||
}
|
||||
.tag.on {
|
||||
color: #6acdbb;
|
||||
background: rgba(106, 205, 187, 0.12);
|
||||
|
||||
.info-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.tag.off {
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
|
||||
.info-text {
|
||||
font-size: 24rpx;
|
||||
color: $text-secondary;
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
align-items: flex-end;
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-top: auto;
|
||||
}
|
||||
.act-btn {
|
||||
font-size: 24rpx;
|
||||
color: #6acdbb;
|
||||
padding: 4rpx 0;
|
||||
|
||||
.action-btn {
|
||||
font-size: 24rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.act-btn.shelf {
|
||||
color: #666;
|
||||
|
||||
.action-btn.outline {
|
||||
color: $text-primary;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid $border-color;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 60rpx;
|
||||
|
||||
.action-btn.btn-primary {
|
||||
color: #ffffff;
|
||||
background: $theme-color;
|
||||
border: 1rpx solid $theme-color;
|
||||
}
|
||||
</style>
|
||||
|
||||
.action-btn.btn-danger {
|
||||
color: $danger-color;
|
||||
background: $danger-color-light;
|
||||
border: 1rpx solid transparent;
|
||||
}
|
||||
|
||||
.btn-hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn-hover-opacity {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background-color: $bg-color;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 用伪元素做一个简单的空状态图形占位,有条件可替换为真实的Image/SVG */
|
||||
.empty-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border: 8rpx solid $border-color;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: $text-light;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -36,6 +36,10 @@
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<view class="p-row-32 m-t-8 white b-r-8">
|
||||
<account-switch-entry />
|
||||
</view>
|
||||
|
||||
<view class="p-row-32 m-t-8" style="height: 100rpx;">
|
||||
<u-button :throttle-time="0" shape="circle" :custom-style="{backgroundColor:'#6ACDBB',color:'#fff',heigth:'96rpx'}" @click="logout">
|
||||
退出登录</u-button>
|
||||
@@ -49,8 +53,10 @@
|
||||
logout,
|
||||
toUpload,
|
||||
} from '@/api/all.js'
|
||||
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
|
||||
import { chooseAvatarImage } from '@/common/js/select-avatar-image.js';
|
||||
export default {
|
||||
components: { AccountSwitchEntry },
|
||||
data() {
|
||||
return {
|
||||
doctorInfo: []
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
40056256325
|
||||
</view>
|
||||
</view>
|
||||
<view class="p-row-32 m-t-8 white">
|
||||
<account-switch-entry />
|
||||
</view>
|
||||
<view class="p-row-32 m-t-8" style="height: 80rpx;">
|
||||
<u-button :throttle-time="0" shape="circle" :custom-style="{backgroundColor:'#6ACDBB',color:'#fff',heigth:'86rpx'}" @click="logout">
|
||||
退出登录</u-button>
|
||||
@@ -33,7 +36,9 @@
|
||||
import {
|
||||
logout
|
||||
} from '@/api/all.js'
|
||||
import AccountSwitchEntry from '@/components/account-switch/account-switch-entry.vue'
|
||||
export default {
|
||||
components: { AccountSwitchEntry },
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
116
utils/loginSession.js
Normal file
116
utils/loginSession.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { getClinicAdminMyInfo } from '@/api/clinicAdmin.js';
|
||||
|
||||
import { getUserInfo } from '@/api/all.js';
|
||||
|
||||
|
||||
|
||||
function applyServiceUserProfile(user) {
|
||||
|
||||
if (!user || !user.id) return;
|
||||
|
||||
uni.setStorageSync('loginInfo', user);
|
||||
|
||||
uni.setStorageSync('role', user.role);
|
||||
|
||||
uni.setStorageSync('login_id', user.id);
|
||||
|
||||
uni.setStorageSync('login_status', user.status);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function saveLoginSession(result) {
|
||||
|
||||
if (!result) return;
|
||||
|
||||
|
||||
|
||||
const token = result.token ? String(result.token).trim() : '';
|
||||
|
||||
|
||||
|
||||
if (result.account_type === 'admin') {
|
||||
|
||||
uni.setStorageSync('loginMode', 'clinic_admin');
|
||||
|
||||
uni.setStorageSync('clinic_admin_token', token);
|
||||
|
||||
uni.setStorageSync('clinic_admin_user', result.user || {});
|
||||
|
||||
uni.removeStorageSync('token');
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
uni.removeStorageSync('loginMode');
|
||||
|
||||
uni.removeStorageSync('clinic_admin_token');
|
||||
|
||||
uni.removeStorageSync('clinic_admin_user');
|
||||
|
||||
uni.setStorageSync('token', token);
|
||||
|
||||
|
||||
|
||||
const user = result.user || {};
|
||||
|
||||
const storeDoctor = result.store_doctor || {};
|
||||
|
||||
uni.setStorageSync('identity', user.role);
|
||||
|
||||
uni.setStorageSync('logInfo', user);
|
||||
|
||||
uni.setStorageSync('store_id', storeDoctor.store_id || '11001');
|
||||
|
||||
if (user.id) {
|
||||
|
||||
uni.setStorageSync('doctor_id', user.id);
|
||||
|
||||
uni.setStorageSync('user_id', user.id);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function fetchProfileAfterLogin(result) {
|
||||
|
||||
if (result.account_type === 'admin') {
|
||||
|
||||
const wrap = await getClinicAdminMyInfo();
|
||||
|
||||
if (wrap && wrap.ok) {
|
||||
|
||||
uni.setStorageSync('clinic_admin_user', wrap.data);
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (result.user && result.user.id) {
|
||||
|
||||
applyServiceUserProfile(result.user);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const data = {
|
||||
|
||||
store_id: uni.getStorageSync('store_id') || '11001',
|
||||
|
||||
};
|
||||
|
||||
const res = await getUserInfo(data);
|
||||
|
||||
Reference in New Issue
Block a user