优化功能
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This commit is contained in:
@@ -3,9 +3,13 @@ import { baseRequestClient, requestClient } from '#/api/request';
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
login_method?: 'phone' | 'login_account' | 'job_number'; // 登录方式类型
|
||||
password?: string;
|
||||
username?: string;
|
||||
phone?: string; // 手机号(当login_method为phone时)
|
||||
code?: string;
|
||||
login_account?: string; // 登录账号(当login_method为login_account时)
|
||||
job_number?: string; // 工号(当login_method为job_number时)
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
@@ -17,25 +21,62 @@ export namespace AuthApi {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
/** 发送验证码返回结果 */
|
||||
export interface SendCodeResult {
|
||||
code?: string; // 开发环境返回验证码
|
||||
accounts?: Array<{
|
||||
id: number;
|
||||
login_account: string;
|
||||
job_number: string;
|
||||
}>; // 如果手机号绑定多个账号,返回账号列表
|
||||
need_select?: boolean; // 是否需要选择账号/工号
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('login', {
|
||||
phone: data.username,
|
||||
const params: any = {
|
||||
login_method: data.login_method || 'phone', // 添加默认值
|
||||
password: data.password,
|
||||
code: data.code,
|
||||
});
|
||||
};
|
||||
|
||||
if (data.login_method === 'phone') {
|
||||
params.phone = data.phone || data.username;
|
||||
} else if (data.login_method === 'login_account') {
|
||||
params.login_account = data.login_account;
|
||||
} else if (data.login_method === 'job_number') {
|
||||
params.job_number = data.job_number;
|
||||
}
|
||||
|
||||
return requestClient.post<AuthApi.LoginResult>('login', params);
|
||||
}
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
export async function sendVerificationCode(username: string) {
|
||||
return requestClient.post('send-verification-code', {
|
||||
username,
|
||||
});
|
||||
export async function sendVerificationCode(params: {
|
||||
login_method?: 'phone' | 'login_account' | 'job_number';
|
||||
username?: string;
|
||||
phone?: string;
|
||||
login_account?: string;
|
||||
job_number?: string;
|
||||
}) {
|
||||
const requestParams: any = {
|
||||
login_method: params.login_method || 'phone',
|
||||
};
|
||||
|
||||
if (params.login_method === 'phone') {
|
||||
requestParams.phone = params.phone || params.username;
|
||||
} else if (params.login_method === 'login_account') {
|
||||
requestParams.login_account = params.login_account;
|
||||
} else if (params.login_method === 'job_number') {
|
||||
requestParams.job_number = params.job_number;
|
||||
}
|
||||
|
||||
return requestClient.post<AuthApi.SendCodeResult>('send-verification-code', requestParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BasicOption } from '@vben/types';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, markRaw, ref, useTemplateRef } from 'vue';
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, type VbenFormSchema } from '@vben/common-ui';
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { sendVerificationCode } from '#/api';
|
||||
import { useAuthStore } from '#/store';
|
||||
import {message} from "ant-design-vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
@@ -20,58 +19,52 @@ const authStore = useAuthStore();
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const loading = ref(false);
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'jack',
|
||||
},
|
||||
];
|
||||
const loginMethod = ref<'phone' | 'login_account' | 'job_number'>('phone');
|
||||
const needSelectAccount = ref(false);
|
||||
const accountList = ref<Array<{ id: number; login_account: string; job_number: string }>>([]);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
/* {
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
fieldName: 'selectAccount',
|
||||
label: $t('authentication.selectAccount'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('vben'),
|
||||
},*/
|
||||
const schema: VbenFormSchema[] = [
|
||||
// 1. Radio选择组(最上面)
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
optionType: 'button',
|
||||
options: [
|
||||
{ label: '手机号登录', value: 'phone' },
|
||||
{ label: '登录账号登录', value: 'login_account' },
|
||||
{ label: '工号登录', value: 'job_number' },
|
||||
],
|
||||
},
|
||||
fieldName: 'login_method',
|
||||
label: '登录方式',
|
||||
rules: z.string().min(1, { message: '请选择登录方式' }),
|
||||
defaultValue: 'phone',
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
loginMethod.value = values.login_method;
|
||||
// 切换登录方式时清空其他字段
|
||||
if (values.login_method === 'phone') {
|
||||
form.setValues({ login_account: '', job_number: '' });
|
||||
} else if (values.login_method === 'login_account') {
|
||||
form.setValues({ username: '', job_number: '' });
|
||||
} else if (values.login_method === 'job_number') {
|
||||
form.setValues({ username: '', login_account: '' });
|
||||
}
|
||||
},
|
||||
triggerFields: ['login_method'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 2. 根据选择动态显示输入框
|
||||
const loginMethodValue = loginMethod.value;
|
||||
if (loginMethodValue === 'phone') {
|
||||
schema.push({
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
if (values.selectAccount) {
|
||||
const findUser = MOCK_USER_OPTIONS.find(
|
||||
(item) => item.value === values.selectAccount,
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: 'qiqi991012',
|
||||
username: '15100000000',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerFields: ['selectAccount'],
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z
|
||||
@@ -80,72 +73,188 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
});
|
||||
} else if (loginMethodValue === 'login_account') {
|
||||
schema.push({
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
placeholder: '请输入登录账号',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
/* .regex(
|
||||
/^(?=.*[A-Z0-9])(?=.*[!@#$%^&*])[A-Z0-9!@#$%^&*]{5,18}$/i,
|
||||
'密码由5-18位数字、字母、特殊字符组成。',
|
||||
)*/
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
fieldName: 'login_account',
|
||||
label: '登录账号',
|
||||
rules: z.string().min(1, { message: '请输入登录账号' }),
|
||||
});
|
||||
} else if (loginMethodValue === 'job_number') {
|
||||
schema.push({
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
return countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
handleSendCode: async () => {
|
||||
// 模拟发送验证码
|
||||
// Simulate sending verification code
|
||||
loading.value = true;
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
loading.value = false;
|
||||
throw new Error('formApi is not ready');
|
||||
}
|
||||
await formApi.validateField('username');
|
||||
const isPhoneReady = await formApi.isFieldValid('username');
|
||||
if (!isPhoneReady) {
|
||||
loading.value = false;
|
||||
throw new Error('Phone number is not Ready');
|
||||
}
|
||||
const password = await formApi.isFieldValid('password');
|
||||
if (!password) {
|
||||
loading.value = false;
|
||||
throw new Error('密码不符合要求 number is not Ready');
|
||||
}
|
||||
const { username } = await formApi.getValues();
|
||||
// 验证username的手机号格式
|
||||
await sendVerificationCode(username);
|
||||
placeholder: '请输入工号',
|
||||
},
|
||||
fieldName: 'job_number',
|
||||
label: '工号',
|
||||
rules: z.string().min(1, { message: '请输入工号' }),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 密码输入框(常驻)
|
||||
schema.push({
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
});
|
||||
|
||||
// 4. 验证码输入框(常驻)
|
||||
schema.push({
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
return countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
handleSendCode: async () => {
|
||||
loading.value = true;
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
loading.value = false;
|
||||
message.success('发送成功,请注意查收');
|
||||
},
|
||||
throw new Error('formApi is not ready');
|
||||
}
|
||||
|
||||
const values = await formApi.getValues();
|
||||
const loginMethodValue = values.login_method || 'phone';
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (loginMethodValue === 'phone') {
|
||||
// 手机号登录:验证手机号
|
||||
await formApi.validateField('username');
|
||||
const isPhoneReady = await formApi.isFieldValid('username');
|
||||
if (!isPhoneReady) {
|
||||
loading.value = false;
|
||||
throw new Error('手机号格式不正确');
|
||||
}
|
||||
const password = await formApi.isFieldValid('password');
|
||||
if (!password) {
|
||||
loading.value = false;
|
||||
throw new Error('密码不符合要求');
|
||||
}
|
||||
result = await sendVerificationCode({
|
||||
login_method: 'phone',
|
||||
username: values.username
|
||||
});
|
||||
} else if (loginMethodValue === 'login_account') {
|
||||
// 登录账号登录:验证登录账号
|
||||
await formApi.validateField('login_account');
|
||||
const isAccountReady = await formApi.isFieldValid('login_account');
|
||||
if (!isAccountReady) {
|
||||
loading.value = false;
|
||||
throw new Error('登录账号不能为空');
|
||||
}
|
||||
const password = await formApi.isFieldValid('password');
|
||||
if (!password) {
|
||||
loading.value = false;
|
||||
throw new Error('密码不符合要求');
|
||||
}
|
||||
result = await sendVerificationCode({
|
||||
login_method: 'login_account',
|
||||
login_account: values.login_account
|
||||
});
|
||||
} else if (loginMethodValue === 'job_number') {
|
||||
// 工号登录:验证工号
|
||||
await formApi.validateField('job_number');
|
||||
const isJobNumberReady = await formApi.isFieldValid('job_number');
|
||||
if (!isJobNumberReady) {
|
||||
loading.value = false;
|
||||
throw new Error('工号不能为空');
|
||||
}
|
||||
const password = await formApi.isFieldValid('password');
|
||||
if (!password) {
|
||||
loading.value = false;
|
||||
throw new Error('密码不符合要求');
|
||||
}
|
||||
result = await sendVerificationCode({
|
||||
login_method: 'job_number',
|
||||
job_number: values.job_number
|
||||
});
|
||||
}
|
||||
|
||||
if (result) {
|
||||
if (result.need_select && result.accounts && result.accounts.length > 1) {
|
||||
needSelectAccount.value = true;
|
||||
accountList.value = result.accounts;
|
||||
message.warning('该手机号绑定了多个账号,请选择登录账号或工号');
|
||||
} else {
|
||||
needSelectAccount.value = false;
|
||||
message.success('发送成功,请注意查收');
|
||||
}
|
||||
// 开发环境显示验证码
|
||||
if (result.code) {
|
||||
message.info(`验证码:${result.code}`, 10);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '发送验证码失败');
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// component: markRaw(SliderCaptcha),
|
||||
// fieldName: 'captcha',
|
||||
// rules: z.boolean().refine((value) => value, {
|
||||
// message: $t('authentication.verifyRequiredTip'),
|
||||
// }),
|
||||
// },
|
||||
];
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
});
|
||||
|
||||
return schema;
|
||||
});
|
||||
|
||||
// 自定义提交处理
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
const loginMethodValue = values.login_method || 'phone';
|
||||
|
||||
// 验证必填字段
|
||||
if (loginMethodValue === 'phone') {
|
||||
if (!values.username || !/^\d{11}$/.test(values.username)) {
|
||||
message.error('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
} else if (loginMethodValue === 'login_account') {
|
||||
if (!values.login_account || values.login_account.trim() === '') {
|
||||
message.error('请输入登录账号');
|
||||
return;
|
||||
}
|
||||
} else if (loginMethodValue === 'job_number') {
|
||||
if (!values.job_number || values.job_number.trim() === '') {
|
||||
message.error('请输入工号');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loginData: any = {
|
||||
login_method: loginMethodValue,
|
||||
password: values.password,
|
||||
code: values.code,
|
||||
};
|
||||
|
||||
// 根据登录方式组装参数
|
||||
if (loginMethodValue === 'phone') {
|
||||
loginData.phone = values.username;
|
||||
} else if (loginMethodValue === 'login_account') {
|
||||
loginData.login_account = values.login_account;
|
||||
} else if (loginMethodValue === 'job_number') {
|
||||
loginData.job_number = values.job_number;
|
||||
}
|
||||
|
||||
await authStore.authLogin(loginData);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -159,6 +268,6 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
:show-register="false"
|
||||
:show-remember-me="false"
|
||||
:show-third-party-login="false"
|
||||
@submit="authStore.authLogin"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -358,3 +358,11 @@ export async function addDoctorMyDiseaseApi(disease_id: number) {
|
||||
export async function deleteDoctorMyDiseaseApi(id: number) {
|
||||
return requestClient.post<any>(`${prefix}delete-my-disease`, { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新医生签名
|
||||
* @param data 签名数据,包含 sign_image 字段
|
||||
*/
|
||||
export async function updateSignatureApi(data: { sign_image: string }) {
|
||||
return requestClient.post<any>(`${prefix}update-signature`, data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 签名查看和更新模态框组件
|
||||
*
|
||||
* @description 用于查看和更新医生签名
|
||||
* @author 系统
|
||||
* @date 2024
|
||||
*/
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import { updateSignatureApi } from '../api';
|
||||
|
||||
// ==================== 表单配置 ====================
|
||||
|
||||
const formProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'Avatar',
|
||||
componentProps: {
|
||||
placeholder: '请上传签名图片',
|
||||
},
|
||||
fieldName: 'sign_image',
|
||||
formItemClass: 'col-span-12',
|
||||
label: '签名图片',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
|
||||
const [Form, formApi] = useVbenForm(formProps);
|
||||
|
||||
// ==================== 模态框配置 ====================
|
||||
|
||||
const currentSignature = ref<string>('');
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const valid = await formApi.validate();
|
||||
if (valid.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
|
||||
await updateSignatureApi({ sign_image: values.sign_image });
|
||||
|
||||
message.success('更新签名成功');
|
||||
|
||||
// 调用回调刷新数据
|
||||
const data = modalApi.getData<{ onSuccess?: () => void }>();
|
||||
if (data?.onSuccess) {
|
||||
data.onSuccess();
|
||||
}
|
||||
|
||||
modalApi.close();
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '更新签名失败');
|
||||
} finally {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<{ signature?: string }>();
|
||||
if (data?.signature) {
|
||||
currentSignature.value = data.signature;
|
||||
// 设置表单初始值
|
||||
formApi.setValues({ sign_image: data.signature });
|
||||
} else {
|
||||
currentSignature.value = '';
|
||||
formApi.resetFields();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
/**
|
||||
* 签名图片URL
|
||||
* 处理base64格式或URL格式
|
||||
*/
|
||||
const signatureImageUrl = computed(() => {
|
||||
if (!currentSignature.value) return '';
|
||||
|
||||
// 如果是base64格式
|
||||
if (currentSignature.value.startsWith('data:')) {
|
||||
return currentSignature.value;
|
||||
}
|
||||
|
||||
// 如果是URL格式
|
||||
if (currentSignature.value.startsWith('http://') || currentSignature.value.startsWith('https://')) {
|
||||
return currentSignature.value;
|
||||
}
|
||||
|
||||
// 其他情况,尝试作为base64处理
|
||||
return `data:image/jpeg;base64,${currentSignature.value}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否有签名
|
||||
*/
|
||||
const hasSignature = computed(() => {
|
||||
return !!currentSignature.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="查看和更新签名" class="w-[600px]">
|
||||
<div class="space-y-6">
|
||||
<!-- 当前签名查看区域 -->
|
||||
<div v-if="hasSignature" class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
当前签名:
|
||||
</div>
|
||||
<div class="flex items-center justify-center rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
|
||||
<Image
|
||||
:src="signatureImageUrl"
|
||||
alt="医生签名"
|
||||
:preview="true"
|
||||
style="max-height: 160px; max-width: 100%; object-fit: contain;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无签名提示 -->
|
||||
<div v-else class="rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center dark:border-gray-600 dark:bg-gray-800">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
暂无签名,请上传签名图片
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更新签名表单区域 -->
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
更新签名:
|
||||
</div>
|
||||
<Form />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 自定义样式 */
|
||||
</style>
|
||||
@@ -15,6 +15,8 @@ import { useUserStore } from '@vben/stores';
|
||||
import EditCommonPrescriptionModal from './components/EditCommonPrescriptionModal.vue';
|
||||
// 常用方新增弹窗组件
|
||||
import AddCommonPrescriptionModal from './components/AddCommonPrescriptionModal.vue';
|
||||
// 签名查看和更新弹窗组件
|
||||
import SignatureModal from './components/SignatureModal.vue';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -201,6 +203,12 @@ const [AddCommonPrescriptionModals, AddCommonPrescriptionModalApi] =
|
||||
connectedComponent: AddCommonPrescriptionModal,
|
||||
});
|
||||
|
||||
// ==================== 签名查看和更新弹窗 ====================
|
||||
|
||||
const [SignatureModals, SignatureModalApi] = useVbenModal({
|
||||
connectedComponent: SignatureModal,
|
||||
});
|
||||
|
||||
/**
|
||||
* 打开新增常用方弹窗
|
||||
*/
|
||||
@@ -296,6 +304,21 @@ const createDoctorOrder = () => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开签名查看和更新弹窗
|
||||
*/
|
||||
const handleViewSignature = () => {
|
||||
const signature = data.value?.identity_info?.sign_image || '';
|
||||
SignatureModalApi.setData({
|
||||
signature,
|
||||
onSuccess: () => {
|
||||
// 刷新医生信息
|
||||
getMyInfo();
|
||||
},
|
||||
});
|
||||
SignatureModalApi.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -321,11 +344,21 @@ const createDoctorOrder = () => {
|
||||
<Card v-if="data && activeTabBar === 1" title="个人资料">
|
||||
<!-- 头部信息 -->
|
||||
<div class="mb-8 flex flex-col gap-6 md:flex-row md:gap-8">
|
||||
<img
|
||||
:src="data?.avatar"
|
||||
alt="医生头像"
|
||||
class="h-24 w-24 rounded-full border-4 border-white object-cover shadow-lg md:h-32 md:w-32"
|
||||
/>
|
||||
<div class="relative">
|
||||
<img
|
||||
:src="data?.avatar"
|
||||
alt="医生头像"
|
||||
class="h-24 w-24 rounded-full border-4 border-white object-cover shadow-lg md:h-32 md:w-32"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="absolute bottom-0 right-0"
|
||||
@click="handleViewSignature"
|
||||
>
|
||||
查看签名
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="mb-3 flex items-center gap-4">
|
||||
@@ -743,6 +776,8 @@ const createDoctorOrder = () => {
|
||||
<EditCommonPrescriptionModals />
|
||||
<!-- 常用方新增弹窗 -->
|
||||
<AddCommonPrescriptionModals />
|
||||
<!-- 签名查看和更新弹窗 -->
|
||||
<SignatureModals />
|
||||
</Page>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -80,6 +80,14 @@ export async function getQuickMenuApi() {
|
||||
return requestClient.get<any>(`${prefix}get-quick-menu`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成登录账号
|
||||
* @param id
|
||||
*/
|
||||
export async function generateLoginAccount(id: number) {
|
||||
return requestClient.post<any>(`${prefix}generate-login-account`, { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑账户绑定银行卡
|
||||
* @param data
|
||||
|
||||
@@ -12,6 +12,7 @@ interface RowType {
|
||||
code: string;
|
||||
platform_id: string;
|
||||
phone: string;
|
||||
login_account: string;
|
||||
desc: string;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -44,6 +45,13 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
{ field: 'platform.name', title: '所属平台' },
|
||||
{ field: 'supplier.name', title: '所属供应商' },
|
||||
{ field: 'phone', title: '手机号码' },
|
||||
{
|
||||
field: 'login_account',
|
||||
align: 'left',
|
||||
title: '登录账号',
|
||||
slots: { default: 'login_account' },
|
||||
width: 150,
|
||||
},
|
||||
{ field: 'email', title: '邮箱' },
|
||||
{ field: 'desc', title: '备注' },
|
||||
{ field: 'created_at', title: '注册时间' },
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, Image, message } from 'ant-design-vue';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import {deleteAdmin, resetPassword} from './api';
|
||||
import { deleteAdmin, resetPassword, generateLoginAccount } from './api';
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
@@ -69,6 +69,40 @@ const resetPasswordApi = (id: number) => {
|
||||
gridApi.reload();
|
||||
});
|
||||
};
|
||||
|
||||
const generateAccountApi = async (row: any) => {
|
||||
try {
|
||||
const result = await generateLoginAccount(row.id);
|
||||
if (result.login_account) {
|
||||
message.success('生成账号成功');
|
||||
gridApi.reload();
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '生成账号失败');
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
message.success('复制成功');
|
||||
} catch (error) {
|
||||
// 降级方案:使用传统方法
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
message.success('复制成功');
|
||||
} catch (err) {
|
||||
message.error('复制失败');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -110,6 +144,16 @@ const resetPasswordApi = (id: number) => {
|
||||
<template #avatar="{ row }">
|
||||
<Image :src="row.avatar" height="30" width="30" />
|
||||
</template>
|
||||
<template #login_account="{ row }">
|
||||
<span v-if="row.login_account">
|
||||
<Button type="link" size="small" @click="copyToClipboard(row.login_account)">
|
||||
{{ row.login_account }}
|
||||
</Button>
|
||||
</span>
|
||||
<Button v-else type="link" size="small" @click="generateAccountApi(row)">
|
||||
生成账号
|
||||
</Button>
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
|
||||
@@ -233,6 +233,7 @@ export const modalFormProps: VbenFormProps = {
|
||||
label: '中药采购价比例',
|
||||
rules: 'required',
|
||||
suffix: () => '%',
|
||||
defaultValue: 100,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
@@ -244,6 +245,7 @@ export const modalFormProps: VbenFormProps = {
|
||||
label: '中药销售比例',
|
||||
rules: 'required',
|
||||
suffix: () => '%',
|
||||
defaultValue: 100,
|
||||
},
|
||||
// {
|
||||
// component: 'VbenInput',
|
||||
|
||||
Reference in New Issue
Block a user