fix: 一些基本功能
This commit is contained in:
@@ -36,6 +36,8 @@ import {
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { registerComponent } from '#/components/form/component-map';
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
@@ -149,6 +151,8 @@ async function initComponentAdapter() {
|
||||
Upload,
|
||||
};
|
||||
|
||||
// 自动注册自定义组件
|
||||
registerComponent(components);
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||
return requestClient.get<RouteRecordStringComponent[]>('auth/menu');
|
||||
}
|
||||
|
||||
17
apps/web-antd/src/api/core/upload.ts
Normal file
17
apps/web-antd/src/api/core/upload.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
* @param data
|
||||
*/
|
||||
export async function uploadFile(data: any) {
|
||||
return requestClient.upload('upload/image', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过文件id集合获取文件信息
|
||||
* @param data
|
||||
*/
|
||||
export async function getFileInfoByIds(data: any) {
|
||||
return requestClient.post('/sys/fileInfo/getFileInfoByIds', data);
|
||||
}
|
||||
@@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/user/info');
|
||||
return requestClient.get<UserInfo>('auth/my-info');
|
||||
}
|
||||
|
||||
@@ -17,9 +17,25 @@ import { message } from 'ant-design-vue';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { refreshTokenApi } from './core';
|
||||
import {Base64} from "js-base64";
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
const arr = new Set([
|
||||
'accept_tel',
|
||||
'doctor',
|
||||
'express_mobile',
|
||||
'id_card',
|
||||
'idcard',
|
||||
'mobile',
|
||||
'password',
|
||||
'patient_mobile',
|
||||
'phone',
|
||||
]);
|
||||
|
||||
// 敏感数据
|
||||
const sensitiveData = new Set(['id_card', 'idcard']);
|
||||
|
||||
function createRequestClient(baseURL: string) {
|
||||
const client = new RequestClient({
|
||||
baseURL,
|
||||
@@ -65,6 +81,15 @@ function createRequestClient(baseURL: string) {
|
||||
|
||||
config.headers.Authorization = formatToken(accessStore.accessToken);
|
||||
config.headers['Accept-Language'] = preferences.app.locale;
|
||||
// 加密
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (config.data != null) {
|
||||
config.data = getRes(config.data, false);
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (config.params != null) {
|
||||
config.params = getRes(config.params, false);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
});
|
||||
@@ -74,9 +99,29 @@ function createRequestClient(baseURL: string) {
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
const { code, data } = responseData;
|
||||
if (status >= 200 && status < 400 && code === 0) {
|
||||
return data;
|
||||
// 这里吧 data 换成了 result
|
||||
const { code, result } = responseData;
|
||||
// 判断返回状态码
|
||||
if (status >= 200 && status < 400) {
|
||||
switch (code) {
|
||||
case 0: {
|
||||
// 解密
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (result != null) {
|
||||
return getRes(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case 401: {
|
||||
// 401 错误,需要重新登录
|
||||
return doReAuthenticate();
|
||||
}
|
||||
case 403:
|
||||
case 404:
|
||||
case 500: {
|
||||
throw Object.assign({}, response, { response });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw Object.assign({}, response, { response });
|
||||
@@ -112,3 +157,119 @@ function createRequestClient(baseURL: string) {
|
||||
export const requestClient = createRequestClient(apiURL);
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
|
||||
/**
|
||||
* 加解密方法
|
||||
* @param obj
|
||||
* @param isDecode
|
||||
*/
|
||||
function getRes(obj: any, isDecode = true) {
|
||||
try {
|
||||
for (const key in obj) {
|
||||
if (Array.isArray(obj[key])) {
|
||||
obj[key] = getRes(obj[key]);
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
obj[key] = getRes(obj[key]);
|
||||
} else {
|
||||
// 判断obj[key]是否在arr中
|
||||
if (arr.has(key)) {
|
||||
let aseFile = '';
|
||||
aseFile = isDecode
|
||||
? customBase64Decode(obj[key])
|
||||
: customBase64Encode(obj[key]);
|
||||
const isGarbled =
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\u0000-\u0008\v\f\u000E-\u001F\u007F-\u00FF\u{E000}-\u{F8FF}]/u.test(
|
||||
aseFile,
|
||||
);
|
||||
if (isGarbled) {
|
||||
aseFile = obj[key];
|
||||
}
|
||||
if (isDecode && sensitiveData.has(key)) {
|
||||
// 数据脱敏,自动匹配姓名、手机号、身份证
|
||||
switch (key) {
|
||||
// case 'accept_name':
|
||||
// case 'express_name':
|
||||
// case 'express_region':
|
||||
// case 'patient': {
|
||||
// // 保留前两个字符,其余用星号代替
|
||||
// aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
|
||||
// break;
|
||||
// }
|
||||
// case 'express_mobile':
|
||||
// case 'mobile': {
|
||||
// // 保留前三位和后四位,中间用星号代替
|
||||
// aseFile =
|
||||
// aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') +
|
||||
// aseFile.slice(-4);
|
||||
// break;
|
||||
// }
|
||||
case 'id_card':
|
||||
case 'idcard': {
|
||||
// 保留前六位和后四位,中间用星号代替
|
||||
aseFile =
|
||||
aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') +
|
||||
aseFile.slice(-4);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// 默认情况下,全部用星号代替
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
obj[key] = aseFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
} catch (error) {
|
||||
console.error('错误', error);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
* @param data
|
||||
*/
|
||||
function customBase64Decode(data: string) {
|
||||
// return data
|
||||
try {
|
||||
// 第一次Base64解码
|
||||
const decodedFirst = Base64.decode(data);
|
||||
// 截取从第30个字符开始往后的内容
|
||||
const subStr = decodedFirst.slice(30);
|
||||
// 第二次Base64解码
|
||||
const base64 = Base64.decode(subStr);
|
||||
return base64.length > 0 ? base64 : data;
|
||||
} catch (error) {
|
||||
console.error(`解码Base64字符串【${data}】时发生错误`, error);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
* @param data
|
||||
*/
|
||||
function customBase64Encode(data: string) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (data == null || data === '') return data;
|
||||
// return data
|
||||
try {
|
||||
// 和加密方法差不多但是是要生成30个随机字符
|
||||
let randomString = '';
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const randomChar = String.fromCodePoint(
|
||||
Math.floor(Math.random() * (126 - 32 + 1)) + 32,
|
||||
);
|
||||
randomString += randomChar;
|
||||
}
|
||||
const encoded = Base64.encode(data);
|
||||
return Base64.encode(randomString + encoded);
|
||||
} catch (error) {
|
||||
console.error(`加密字符串【${data}】时发生错误`, error);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
77
apps/web-antd/src/components/description/description.vue
Normal file
77
apps/web-antd/src/components/description/description.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import type { DescItem } from './types';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
|
||||
import { componentMap } from '#/components/view/component-map';
|
||||
|
||||
defineProps({
|
||||
title: { type: String, default: '' },
|
||||
bordered: { type: Boolean, default: true },
|
||||
size: {
|
||||
type: String as PropType<'default' | 'middle' | 'small'>,
|
||||
default: undefined,
|
||||
},
|
||||
column: {
|
||||
type: [Number, Object],
|
||||
default: () => {
|
||||
// return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
||||
return 12;
|
||||
},
|
||||
},
|
||||
labelStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
width: '120px',
|
||||
};
|
||||
},
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
width: '0px',
|
||||
};
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
type: Array as PropType<DescItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
data: { type: Object, default: undefined },
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Descriptions
|
||||
:bordered="bordered"
|
||||
:column="column"
|
||||
:content-style="contentStyle"
|
||||
:label-style="labelStyle"
|
||||
:size="size"
|
||||
:title="title ? title : undefined"
|
||||
>
|
||||
<template v-for="item in schema" :key="item.field">
|
||||
<DescriptionsItem :label="item.label" :span="item.span">
|
||||
<component
|
||||
:is="(componentMap as Map<String, any>).get(item.component)"
|
||||
v-if="(componentMap as Map<String, any>).has(item.component)"
|
||||
:value="data?.[item.field]"
|
||||
v-bind="{ ...item.componentProps }"
|
||||
/>
|
||||
<component
|
||||
:is="item.render(data?.[item.field], data)"
|
||||
v-else-if="
|
||||
!(componentMap as Map<String, any>).has(item.component) &&
|
||||
item.render
|
||||
"
|
||||
:value="data?.[item.field]"
|
||||
v-bind="{ ...item.componentProps }"
|
||||
/>
|
||||
<template v-else>{{ data?.[item.field] }}</template>
|
||||
</DescriptionsItem>
|
||||
</template>
|
||||
</Descriptions>
|
||||
</template>
|
||||
2
apps/web-antd/src/components/description/index.ts
Normal file
2
apps/web-antd/src/components/description/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Description } from './description.vue';
|
||||
export type * from './types';
|
||||
54
apps/web-antd/src/components/description/types.d.ts
vendored
Normal file
54
apps/web-antd/src/components/description/types.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { CollapseContainerOptions } from '@/components/Container';
|
||||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
|
||||
|
||||
import type { CSSProperties, VNode } from 'vue';
|
||||
|
||||
export interface DescItem {
|
||||
labelMinWidth?: number;
|
||||
contentMinWidth?: number;
|
||||
labelStyle?: CSSProperties;
|
||||
field: string;
|
||||
label: JSX.Element | string | VNode;
|
||||
// Merge column
|
||||
span?: number;
|
||||
show?: (...arg: any) => boolean;
|
||||
// render
|
||||
render?: (
|
||||
val: any,
|
||||
data: Recordable,
|
||||
) => Element | JSX.Element | number | string | undefined | VNode;
|
||||
component: string;
|
||||
componentProps?: any;
|
||||
children?: DescItem[];
|
||||
}
|
||||
|
||||
export interface DescriptionProps extends DescriptionsProps {
|
||||
// Whether to include the collapse component
|
||||
useCollapse?: boolean;
|
||||
/**
|
||||
* item configuration
|
||||
* @type DescItem
|
||||
*/
|
||||
schema: DescItem[];
|
||||
/**
|
||||
* 数据
|
||||
* @type object
|
||||
*/
|
||||
data: Recordable;
|
||||
/**
|
||||
* Built-in CollapseContainer component configuration
|
||||
* @type CollapseContainerOptions
|
||||
*/
|
||||
collapseOptions?: CollapseContainerOptions;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
}
|
||||
|
||||
export type Register = (descInstance: DescInstance) => void;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
*/
|
||||
export type UseDescReturnType = [Register, DescInstance];
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, onMounted, watch } from 'vue';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
|
||||
import { Spin } from 'ant-design-vue';
|
||||
|
||||
import { Description } from '#/components/description';
|
||||
import { useSchemaStore } from '#/store/schema';
|
||||
import { schemaToDetailForm } from '#/util/tool';
|
||||
|
||||
const props = defineProps({
|
||||
tableName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
view: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
addFieldPrefix: {
|
||||
// 追加字段前辍
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const schemaStore = useSchemaStore();
|
||||
const schema = computed(() => {
|
||||
return schemaStore.getSchema(props.tableName);
|
||||
});
|
||||
const schemaDescItems = computed(() => {
|
||||
return schemaToDetailForm(schema.value, props.data);
|
||||
});
|
||||
|
||||
const formObj = computed(() => {
|
||||
const [Form, api] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
...schema.value,
|
||||
});
|
||||
return {
|
||||
form: Form,
|
||||
api,
|
||||
};
|
||||
});
|
||||
const cData = computed(() => {
|
||||
const newData: any = {};
|
||||
Object.keys(props.data || {}).forEach((key: string) => {
|
||||
// 如果key不以addFieldPrefix为前辍,则自动追加
|
||||
if (key.startsWith(props.addFieldPrefix)) {
|
||||
newData[key] = props.data[key];
|
||||
} else {
|
||||
newData[props.addFieldPrefix + key] = props.data[key];
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
});
|
||||
watch(
|
||||
() => props.tableName,
|
||||
() => {
|
||||
schemaStore.requestData(props.tableName);
|
||||
},
|
||||
);
|
||||
onMounted(() => {
|
||||
schemaStore.requestData(props.tableName);
|
||||
});
|
||||
defineExpose({
|
||||
getForm() {
|
||||
return markRaw(formObj.value?.form);
|
||||
},
|
||||
getFormApi() {
|
||||
return formObj.value?.api;
|
||||
},
|
||||
});
|
||||
const spinning = computed(() => {
|
||||
return Object.keys(schema.value).length === 0;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Spin :spinning="spinning">
|
||||
<div v-if="view" class="desc-wrap">
|
||||
<div v-for="item in schemaDescItems" :key="item.field" class="desc-card">
|
||||
<Description
|
||||
:data="cData"
|
||||
:label-style="{
|
||||
width: '120px',
|
||||
}"
|
||||
:schema="item.children"
|
||||
:title="schemaDescItems.length === 1 ? undefined : item.label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Spin v-else :spinning="spinning">
|
||||
<component :is="markRaw(formObj?.form)" />
|
||||
</Spin>
|
||||
</Spin>
|
||||
</template>
|
||||
<style scoped lang="less">
|
||||
@import '#/assets/styles/common-form.less';
|
||||
@import '#/assets/styles/common-detail.less';
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DynamicSchemaForm } from './dynamic-schema-form.vue';
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import { onMounted, type PropType, ref, watch } from 'vue';
|
||||
|
||||
import { schemaToDetailForm } from '#/util/tool';
|
||||
|
||||
import { type DescItem, Description } from '../description';
|
||||
|
||||
const props = defineProps({
|
||||
formOptions: {
|
||||
type: Object as PropType<VbenFormProps>,
|
||||
default: () => ({}),
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
descriptionProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const schemaDescItems = ref<DescItem[]>([]);
|
||||
const initialSchemaDescItems = () => {
|
||||
if (
|
||||
Object.keys(props.formOptions).length > 0 &&
|
||||
Object.keys(props.data).length > 0
|
||||
) {
|
||||
schemaDescItems.value = schemaToDetailForm(props.formOptions, props.data);
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
initialSchemaDescItems();
|
||||
});
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
initialSchemaDescItems();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.formOptions,
|
||||
() => {
|
||||
initialSchemaDescItems();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="desc-wrap">
|
||||
<div
|
||||
v-for="schema in schemaDescItems"
|
||||
:key="schema.field"
|
||||
class="desc-card"
|
||||
>
|
||||
<Description
|
||||
:data="data"
|
||||
:label-style="{
|
||||
width: '150px',
|
||||
}"
|
||||
:schema="schema.children"
|
||||
:title="schemaDescItems.length === 1 ? undefined : schema.label"
|
||||
v-bind="descriptionProps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
1
apps/web-antd/src/components/form-description/index.ts
Normal file
1
apps/web-antd/src/components/form-description/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as FormDescription } from './form-description.vue';
|
||||
32
apps/web-antd/src/components/form/component-map.ts
Normal file
32
apps/web-antd/src/components/form/component-map.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { CustomComponentType } from './types';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool';
|
||||
|
||||
const componentMap = new Map<CustomComponentType | string, Component>();
|
||||
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
|
||||
const modules = import.meta.glob(
|
||||
['./components/**/*.vue', '../../views/**/components/form/*.vue'],
|
||||
{ eager: true },
|
||||
);
|
||||
// 加入到路由集合中
|
||||
Object.keys(modules).forEach((key) => {
|
||||
if (!key.includes('-ignore')) {
|
||||
const mod = (modules as any)[key].default || {};
|
||||
// ./components/ApiDict.vue
|
||||
// 获取ApiDict
|
||||
const compName = getFileNameWithoutExtension(key);
|
||||
componentMap.set(toPascalCase(compName), mod);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 注册组件
|
||||
* @param components
|
||||
*/
|
||||
export const registerComponent = (components: any) => {
|
||||
componentMap.forEach((value, key) => {
|
||||
components[key] = value as Component;
|
||||
});
|
||||
};
|
||||
export { componentMap };
|
||||
62
apps/web-antd/src/components/form/components/avatar.vue
Normal file
62
apps/web-antd/src/components/form/components/avatar.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
import { uploadFile } from '#/api/core/upload';
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['update:value']);
|
||||
const mValue = useVModel(props, 'value', emits, {
|
||||
defaultValue: props.value,
|
||||
passive: true,
|
||||
});
|
||||
const customRequest = (e: any) => {
|
||||
uploadFile({
|
||||
file: e.file,
|
||||
}).then((data: any) => {
|
||||
mValue.value = data.url;
|
||||
});
|
||||
};
|
||||
const handleRemove = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
mValue.value = '';
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Upload
|
||||
:custom-request="customRequest"
|
||||
:show-upload-list="false"
|
||||
list-type="picture-card"
|
||||
>
|
||||
<div v-if="mValue" class="m-avatar-wrap">
|
||||
<Icon
|
||||
class="m-avatar-icon-delete"
|
||||
icon="ant-design:delete-outlined"
|
||||
@click="handleRemove"
|
||||
/>
|
||||
<img :src="value" width="100%" />
|
||||
</div>
|
||||
<Icon v-else icon="ant-design:plus-outlined" />
|
||||
</Upload>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.m-avatar-wrap {
|
||||
position: relative;
|
||||
height: 102px;
|
||||
.m-avatar-icon-delete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
162
apps/web-antd/src/components/form/components/upload.vue
Normal file
162
apps/web-antd/src/components/form/components/upload.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, ref, useAttrs, watch } from 'vue';
|
||||
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import {
|
||||
message,
|
||||
Upload,
|
||||
type UploadChangeParam,
|
||||
type UploadFile,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getFileInfoByIds } from '#/api/core/upload';
|
||||
import { Icon } from '#/components/icon';
|
||||
import { omit } from '#/util/tool';
|
||||
|
||||
defineOptions({
|
||||
name: 'Upload',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
const props = defineProps({
|
||||
value: {
|
||||
// 文件id,多个使用英文逗号分割
|
||||
type: [String] as PropType<string>,
|
||||
default: undefined,
|
||||
},
|
||||
action: {
|
||||
// 上传的地址
|
||||
type: String,
|
||||
default: `${import.meta.env.VITE_GLOB_API_URL}/sys/fileInfo/upload`,
|
||||
},
|
||||
headers: {
|
||||
// 上传请求的 headers
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
disabled: {
|
||||
// 是否禁用
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listType: {
|
||||
// 上传类型
|
||||
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||
default: 'text',
|
||||
},
|
||||
maxCount: {
|
||||
// 最大上传数量
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
uploadName: {
|
||||
// 文件上传字段名,name被字段名覆写了,要换成uploadName
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
separator: {
|
||||
// 文件id分隔符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const accessStore = useAccessStore();
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const bindProps = computed(() => {
|
||||
return {
|
||||
...omit(attrs, ['onChange', 'onInput', 'onBlur', 'name']),
|
||||
...props,
|
||||
name: props.uploadName || 'file',
|
||||
};
|
||||
});
|
||||
const _getFileInfoByIds = (val: string | string[] | undefined) => {
|
||||
if (!val) return;
|
||||
getFileInfoByIds({
|
||||
fileInfoIds: val,
|
||||
}).then((res) => {
|
||||
fileList.value = res || [];
|
||||
});
|
||||
};
|
||||
const mValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emits('update:modelValue', val);
|
||||
},
|
||||
});
|
||||
const mHeaders = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
...props.headers,
|
||||
};
|
||||
const handleChange = (info: UploadChangeParam) => {
|
||||
if (info.file.status === 'done') {
|
||||
// 更新file-list当前file的uid
|
||||
fileList.value = fileList.value.map((item) => {
|
||||
if (item.uid === info.file.uid) {
|
||||
return {
|
||||
...item,
|
||||
uid: info.file.response.data.fileInfoId,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
mValue.value = fileList.value
|
||||
.map((item) => {
|
||||
return item.uid;
|
||||
})
|
||||
.join(props.separator);
|
||||
} else if (info.file.status === 'error') {
|
||||
message.error(`${info.file.name} file upload failed.`);
|
||||
}
|
||||
};
|
||||
const handleRemove = (file: any) => {
|
||||
mValue.value = fileList.value
|
||||
.filter((item: any) => item.uid !== file.uid)
|
||||
.map((item: any) => item.uid)
|
||||
.join(props.separator);
|
||||
return true;
|
||||
};
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
_getFileInfoByIds(val);
|
||||
},
|
||||
);
|
||||
onMounted(() => {
|
||||
_getFileInfoByIds(props.value);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Upload
|
||||
v-model:file-list="fileList"
|
||||
v-bind="bindProps"
|
||||
:action="action"
|
||||
:disabled="disabled"
|
||||
:headers="mHeaders"
|
||||
:list-type="listType"
|
||||
:max-count="maxCount"
|
||||
@change="handleChange"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<template #default>
|
||||
<Icon
|
||||
v-if="listType === 'picture-card'"
|
||||
:size="24"
|
||||
icon="ant-design:plus-outlined"
|
||||
/>
|
||||
<Button v-else>
|
||||
上传文件
|
||||
<template #icon>
|
||||
<Icon icon="ant-design:cloud-upload-outlined" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
<template v-for="item in Object.keys($slots)" #[item]="data">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</Upload>
|
||||
</template>
|
||||
7
apps/web-antd/src/components/form/index.ts
Normal file
7
apps/web-antd/src/components/form/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// export { default as ApiCheckboxGroup } from './components/api-checkbox-group.vue';
|
||||
// export { default as ApiDict } from './components/api-dict.vue';
|
||||
// export { default as ApiRadioGroup } from './components/api-radio-group.vue';
|
||||
// export { default as ApiSelect } from './components/api-select.vue';
|
||||
// export { default as ApiTreeSelect } from './components/api-tree-select.vue';
|
||||
// export { default as IconPicker } from './components/icon-picker.vue';
|
||||
export { default as Upload } from './components/upload.vue';
|
||||
7
apps/web-antd/src/components/form/types/index.d.ts
vendored
Normal file
7
apps/web-antd/src/components/form/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export type CustomComponentType =
|
||||
| 'ApiCheckboxGroup'
|
||||
| 'ApiDict'
|
||||
| 'ApiRadioGroup'
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'IconPicker';
|
||||
51
apps/web-antd/src/components/icon/icon.vue
Normal file
51
apps/web-antd/src/components/icon/icon.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue';
|
||||
|
||||
import { IconifyIcon as VbenIcon } from '@vben/icons';
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '16px',
|
||||
},
|
||||
});
|
||||
const iconComp = computed(() => {
|
||||
if (props.icon.startsWith('http')) {
|
||||
return () => h('img', { src: props.icon, class: 'm-icon__' });
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const styles = computed(() => {
|
||||
return {
|
||||
fontSize: props.size.toString().endsWith('px')
|
||||
? props.size
|
||||
: `${props.size}px`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="iconComp" v-if="iconComp" :style="styles" />
|
||||
<VbenIcon v-else :icon="props.icon" :style="styles" class="m-icon__" />
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.m-icon__ {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-style: normal;
|
||||
line-height: 0;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
vertical-align: -0.125em;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
1
apps/web-antd/src/components/icon/index.ts
Normal file
1
apps/web-antd/src/components/icon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Icon } from './icon.vue';
|
||||
2
apps/web-antd/src/components/table-action/index.ts
Normal file
2
apps/web-antd/src/components/table-action/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as TableAction } from './table-action.vue';
|
||||
export type * from './types.d.ts';
|
||||
225
apps/web-antd/src/components/table-action/table-action.vue
Normal file
225
apps/web-antd/src/components/table-action/table-action.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import type { ButtonType } from 'ant-design-vue/es/button';
|
||||
|
||||
import type { ActionItem, PopConfirm } from './types';
|
||||
|
||||
import { computed, type PropType, toRaw } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { isBoolean, isFunction } from '@vben/utils';
|
||||
|
||||
import { Button, Dropdown, Menu, Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
const props = defineProps({
|
||||
actions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
dropDownActions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const MenuItem = Menu.Item;
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
|
||||
let isIfShow = true;
|
||||
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
const getActions = computed(() => {
|
||||
return (toRaw(props.actions) || [])
|
||||
.filter((action) => {
|
||||
return (
|
||||
(hasAccessByCodes(action.auth || []) ||
|
||||
(action.auth || []).length === 0) &&
|
||||
isIfShow(action)
|
||||
);
|
||||
})
|
||||
.map((action) => {
|
||||
const { popConfirm } = action;
|
||||
return {
|
||||
// getPopupContainer: document.body,
|
||||
type: 'link' as ButtonType,
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
enable: !!popConfirm,
|
||||
};
|
||||
});
|
||||
});
|
||||
const getDropdownList = computed((): any[] => {
|
||||
return (toRaw(props.dropDownActions) || [])
|
||||
.filter((action) => {
|
||||
return (
|
||||
(hasAccessByCodes(action.auth || []) ||
|
||||
(action.auth || []).length === 0) &&
|
||||
isIfShow(action)
|
||||
);
|
||||
})
|
||||
.map((action, index) => {
|
||||
const { label, popConfirm } = action;
|
||||
return {
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
text: label,
|
||||
divider:
|
||||
index < props.dropDownActions.length - 1 ? props.divider : false,
|
||||
};
|
||||
});
|
||||
});
|
||||
const getPopConfirmProps = (attrs: PopConfirm) => {
|
||||
const originAttrs: any = attrs;
|
||||
delete originAttrs.icon;
|
||||
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||
originAttrs.onConfirm = attrs.confirm;
|
||||
delete originAttrs.confirm;
|
||||
}
|
||||
if (attrs.cancel && isFunction(attrs.cancel)) {
|
||||
originAttrs.onCancel = attrs.cancel;
|
||||
delete originAttrs.cancel;
|
||||
}
|
||||
return originAttrs;
|
||||
};
|
||||
const getButtonProps = (action: ActionItem) => {
|
||||
const res = {
|
||||
type: action.type || 'primary',
|
||||
...action,
|
||||
};
|
||||
delete res.icon;
|
||||
return res;
|
||||
};
|
||||
const handleMenuClick = (e: any) => {
|
||||
const action = getDropdownList.value[e.key];
|
||||
if (action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-table-action">
|
||||
<Space
|
||||
:size="
|
||||
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
|
||||
"
|
||||
>
|
||||
<template v-for="(action, index) in getActions" :key="index">
|
||||
<Popconfirm
|
||||
v-if="action.popConfirm"
|
||||
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||
>
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<Icon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<Button v-bind="getButtonProps(action)">
|
||||
<template v-if="action.icon" #icon>
|
||||
<Icon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button v-else v-bind="getButtonProps(action)" @click="action.onClick">
|
||||
<template v-if="action.icon" #icon>
|
||||
<Icon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</template>
|
||||
</Space>
|
||||
|
||||
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||
<slot name="more">
|
||||
<Button size="small" type="link">
|
||||
<template #icon>
|
||||
<Icon class="icon-more" icon="ant-design:more-outlined" />
|
||||
</template>
|
||||
</Button>
|
||||
</slot>
|
||||
<template #overlay>
|
||||
<Menu @click="handleMenuClick">
|
||||
<MenuItem v-for="(action, index) in getDropdownList" :key="index">
|
||||
<template v-if="action.popConfirm">
|
||||
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<Icon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<div
|
||||
:class="
|
||||
action.disabled === true
|
||||
? 'cursor-not-allowed text-gray-300'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<Icon v-if="action.icon" :icon="action.icon" />
|
||||
<span class="ml-1">{{ action.text }}</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
:class="
|
||||
action.disabled === true
|
||||
? 'cursor-not-allowed text-gray-300'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<Icon v-if="action.icon" :icon="action.icon" />
|
||||
{{ action.label }}
|
||||
</div>
|
||||
</template>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less">
|
||||
/** 修复 iconify 位置问题 **/
|
||||
.m-table-action {
|
||||
.ant-btn > .iconify + span,
|
||||
.ant-btn > span + .iconify {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.ant-btn > .iconify {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-style: normal;
|
||||
line-height: 0;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
vertical-align: -0.125em;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
||||
import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
|
||||
|
||||
export interface PopConfirm {
|
||||
title: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
confirm: Fn;
|
||||
cancel?: Fn;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
export interface ActionItem extends ButtonProps {
|
||||
onClick?: Fn;
|
||||
label?: string;
|
||||
color?: 'error' | 'success' | 'warning';
|
||||
icon?: string;
|
||||
popConfirm?: PopConfirm;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
// 权限编码控制是否显示
|
||||
auth?: string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: ((action: ActionItem) => boolean) | boolean;
|
||||
tooltip?: string | TooltipProps;
|
||||
}
|
||||
22
apps/web-antd/src/components/view/component-map.ts
Normal file
22
apps/web-antd/src/components/view/component-map.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool';
|
||||
|
||||
const componentMap = new Map<string, Component>();
|
||||
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
|
||||
const modules = import.meta.glob(
|
||||
['./components/**/*.vue', '../../views/**/components/view/*.vue'],
|
||||
{ eager: true },
|
||||
);
|
||||
// 加入到路由集合中
|
||||
Object.keys(modules).forEach((key) => {
|
||||
if (!key.includes('-ignore')) {
|
||||
const mod = (modules as any)[key].default || {};
|
||||
// ./components/ApiDict.vue
|
||||
// 获取ApiDict
|
||||
const compName = getFileNameWithoutExtension(key);
|
||||
componentMap.set(toPascalCase(compName), mod);
|
||||
}
|
||||
});
|
||||
|
||||
export { componentMap };
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, ref } from 'vue';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
api: {
|
||||
// 接口请求对象
|
||||
type: [Function, String] as PropType<
|
||||
((...arg: any) => Promise<any>) | String
|
||||
>,
|
||||
default() {
|
||||
return () => {
|
||||
return new Promise((resolve) => {
|
||||
resolve([]);
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
requestMethod: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
// 分割符
|
||||
split: {
|
||||
type: String,
|
||||
default: '/',
|
||||
},
|
||||
});
|
||||
const currentData = ref([]);
|
||||
const cValue = computed(() => {
|
||||
return currentData.value
|
||||
.map((item) => item[props.labelField])
|
||||
.join(props.split);
|
||||
});
|
||||
onMounted(() => {
|
||||
const api: (...arg: any) => Promise<any> =
|
||||
typeof props.api === 'string'
|
||||
? (params: any) => {
|
||||
return (requestClient as any)[props.requestMethod](
|
||||
props.api as any,
|
||||
params,
|
||||
);
|
||||
}
|
||||
: (props.api as (...arg: any) => Promise<any>);
|
||||
const searchType = 'IN';
|
||||
const params =
|
||||
props.requestMethod === 'get'
|
||||
? {
|
||||
params: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
},
|
||||
}
|
||||
: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
};
|
||||
api(params).then((res) => {
|
||||
if (res.length > 0) {
|
||||
currentData.value = res;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import ApiSelect from './api-select.vue';
|
||||
</script>
|
||||
<template>
|
||||
<ApiSelect />
|
||||
</template>
|
||||
67
apps/web-antd/src/components/view/components/api-dict.vue
Normal file
67
apps/web-antd/src/components/view/components/api-dict.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { useDictStore } from '#/store/dict';
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
split: {
|
||||
// 分割符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
join: {
|
||||
// 连接符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
const cValue = computed(() => {
|
||||
if (!props.value && props.value !== 0) {
|
||||
return '';
|
||||
}
|
||||
const arr: Array<any> = [];
|
||||
if (Array.isArray(props.value)) {
|
||||
arr.push(...props.value);
|
||||
} else {
|
||||
arr.push(...props.value.toString().split(props.split));
|
||||
}
|
||||
const dictData = dictStore.getDictData(props.code as string);
|
||||
const res: Array<any> = [];
|
||||
arr.forEach((item) => {
|
||||
for (let i = 0; i < dictData.length; i++) {
|
||||
if (dictData[i].value?.toString() === item?.toString()) {
|
||||
res.push(dictData[i].label);
|
||||
break;
|
||||
}
|
||||
if (i === dictData.length - 1) {
|
||||
res.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.join(props.join);
|
||||
});
|
||||
onMounted(() => {
|
||||
dictStore.requestData(props.code as string);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import ApiSelect from './api-select.vue';
|
||||
</script>
|
||||
<template>
|
||||
<ApiSelect />
|
||||
</template>
|
||||
153
apps/web-antd/src/components/view/components/api-select.vue
Normal file
153
apps/web-antd/src/components/view/components/api-select.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, watch } from 'vue';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
import { useDictStore } from '#/store/dict';
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [Boolean, String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
split: {
|
||||
// 分割符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
join: {
|
||||
// 连接符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
api: {
|
||||
// 接口请求对象
|
||||
type: [Function, String] as PropType<
|
||||
((...arg: any) => Promise<any>) | String
|
||||
>,
|
||||
default() {
|
||||
return () => {
|
||||
return new Promise((resolve) => {
|
||||
resolve([]);
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
requestMethod: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
/**
|
||||
* 获取包含的id
|
||||
*/
|
||||
const getIncludeIds = () => {
|
||||
if (!props.value && props.value !== 0) {
|
||||
return [];
|
||||
}
|
||||
const arr: Array<any> = [];
|
||||
if (Array.isArray(props.value)) {
|
||||
arr.push(...props.value);
|
||||
} else {
|
||||
arr.push(...props.value.toString().split(props.split));
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
/**
|
||||
* 获取缓存key
|
||||
*/
|
||||
const getCacheKey = () => {
|
||||
let cacheKey = props.cacheKey;
|
||||
if (typeof props.api === 'string' && !cacheKey) {
|
||||
cacheKey = props.api as string;
|
||||
}
|
||||
return cacheKey;
|
||||
};
|
||||
const cValue = computed(() => {
|
||||
if (!props.value && props.value !== 0) {
|
||||
return '';
|
||||
}
|
||||
const arr: Array<any> = getIncludeIds();
|
||||
const cacheKey = getCacheKey();
|
||||
const dictData = dictStore.getSelectData(cacheKey, {
|
||||
...props.params,
|
||||
includeType: 2,
|
||||
includeIds: getIncludeIds(),
|
||||
});
|
||||
const res: Array<any> = [];
|
||||
arr.forEach((item) => {
|
||||
for (let i = 0; i < dictData.length; i++) {
|
||||
if (dictData[i].value?.toString() === item?.toString()) {
|
||||
res.push(dictData[i].label);
|
||||
break;
|
||||
}
|
||||
if (i === dictData.length - 1) {
|
||||
res.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.join(props.join);
|
||||
});
|
||||
const requestData = () => {
|
||||
const api: (...arg: any) => Promise<any> =
|
||||
typeof props.api === 'string'
|
||||
? (params: any) => {
|
||||
return (requestClient as any)[props.requestMethod](
|
||||
props.api as any,
|
||||
params,
|
||||
);
|
||||
}
|
||||
: (props.api as (...arg: any) => Promise<any>);
|
||||
const cacheKey = getCacheKey();
|
||||
const params =
|
||||
props.requestMethod === 'get'
|
||||
? {
|
||||
params: {
|
||||
...props.params,
|
||||
includeType: 2,
|
||||
includeIds: getIncludeIds(),
|
||||
},
|
||||
}
|
||||
: {
|
||||
...props.params,
|
||||
includeType: 2,
|
||||
includeIds: getIncludeIds(),
|
||||
};
|
||||
dictStore.select(api, params, cacheKey);
|
||||
};
|
||||
onMounted(() => {
|
||||
requestData();
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
requestData();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
102
apps/web-antd/src/components/view/components/api-tree-select.vue
Normal file
102
apps/web-antd/src/components/view/components/api-tree-select.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, ref } from 'vue';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
api: {
|
||||
// 接口请求对象
|
||||
type: [Function, String] as PropType<
|
||||
((...arg: any) => Promise<any>) | String
|
||||
>,
|
||||
default() {
|
||||
return () => {
|
||||
return new Promise((resolve) => {
|
||||
resolve([]);
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
requestMethod: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 分割符
|
||||
split: {
|
||||
type: String,
|
||||
default: '/',
|
||||
},
|
||||
});
|
||||
const currentData = ref([]);
|
||||
const cValue = computed(() => {
|
||||
return currentData.value
|
||||
.map((item) => item[props.labelField])
|
||||
.join(props.split);
|
||||
});
|
||||
onMounted(() => {
|
||||
const api: (...arg: any) => Promise<any> =
|
||||
typeof props.api === 'string'
|
||||
? (params: any) => {
|
||||
return (requestClient as any)[props.requestMethod](
|
||||
props.api as any,
|
||||
params,
|
||||
);
|
||||
}
|
||||
: (props.api as (...arg: any) => Promise<any>);
|
||||
const searchType = props.multiple ? 'IN' : 'EQ';
|
||||
const params =
|
||||
props.requestMethod === 'get'
|
||||
? {
|
||||
params: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
},
|
||||
}
|
||||
: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
};
|
||||
api(params).then((res) => {
|
||||
if (res.length > 0) {
|
||||
currentData.value = res;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
14
apps/web-antd/src/components/view/components/avatar.vue
Normal file
14
apps/web-antd/src/components/view/components/avatar.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Image } from 'ant-design-vue';
|
||||
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Image v-if="value" :src="value" :width="102" />
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
22
apps/web-antd/src/components/view/components/icon-picker.vue
Normal file
22
apps/web-antd/src/components/view/components/icon-picker.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '16px',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Icon v-if="value" :icon="value" :size="size" />
|
||||
<span v-else></span>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Select from './select.vue';
|
||||
</script>
|
||||
<template>
|
||||
<Select />
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
35
apps/web-antd/src/components/view/components/select.vue
Normal file
35
apps/web-antd/src/components/view/components/select.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Boolean, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
join: {
|
||||
// 连接符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
});
|
||||
const cValue = computed(() => {
|
||||
return Array.isArray(props.value)
|
||||
? props.options
|
||||
.filter((item: any) => (props.value as Array<any>).includes(item.value))
|
||||
.map((item: any) => item.label)
|
||||
.join(props.join)
|
||||
: (
|
||||
props.options.find(
|
||||
(item: any) => item.value?.toString() === props.value?.toString(),
|
||||
) as any
|
||||
)?.label;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
25
apps/web-antd/src/components/view/components/switch.vue
Normal file
25
apps/web-antd/src/components/view/components/switch.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, type PropType } from 'vue';
|
||||
|
||||
import { Switch } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 值
|
||||
value: {
|
||||
type: [String, Number, Boolean] as PropType<boolean | number | string>,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:value']);
|
||||
const mValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emit('update:value', val);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Switch v-model:checked="mValue" :disabled="true" />
|
||||
</template>
|
||||
25
apps/web-antd/src/components/view/components/upload.vue
Normal file
25
apps/web-antd/src/components/view/components/upload.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { Upload } from '#/components/form';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||
default: 'text',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Upload :disabled="true" :list-type="listType" :value="value">
|
||||
<template #default></template>
|
||||
</Upload>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -4,9 +4,9 @@ import type { NotificationItem } from '@vben/layouts';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
// import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||||
// import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||||
import {
|
||||
BasicLayout,
|
||||
LockScreen,
|
||||
@@ -15,41 +15,42 @@ import {
|
||||
} from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
// import { openWindow } from '@vben/utils';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
// import { $t } from '#/locales';
|
||||
import { useAuthStore } from '#/store';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const notifications = ref<NotificationItem[]>([
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||
date: '3小时前',
|
||||
isRead: true,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '收到了 14 份新周报',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '刚刚',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '朱偏右 回复了你',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '2024-01-01',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '曲丽丽 评论了你',
|
||||
},
|
||||
{
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '代办提醒',
|
||||
},
|
||||
// TODO 通知列表
|
||||
// {
|
||||
// avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||
// date: '3小时前',
|
||||
// isRead: true,
|
||||
// message: '描述信息描述信息描述信息',
|
||||
// title: '收到了 14 份新周报',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'https://avatar.vercel.sh/1',
|
||||
// date: '刚刚',
|
||||
// isRead: false,
|
||||
// message: '描述信息描述信息描述信息',
|
||||
// title: '朱偏右 回复了你',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'https://avatar.vercel.sh/1',
|
||||
// date: '2024-01-01',
|
||||
// isRead: false,
|
||||
// message: '描述信息描述信息描述信息',
|
||||
// title: '曲丽丽 评论了你',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'https://avatar.vercel.sh/satori',
|
||||
// date: '1天前',
|
||||
// isRead: false,
|
||||
// message: '描述信息描述信息描述信息',
|
||||
// title: '代办提醒',
|
||||
// },
|
||||
]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
@@ -61,33 +62,34 @@ const showDot = computed(() =>
|
||||
);
|
||||
|
||||
const menus = computed(() => [
|
||||
{
|
||||
handler: () => {
|
||||
openWindow(VBEN_DOC_URL, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: BookOpenText,
|
||||
text: $t('ui.widgets.document'),
|
||||
},
|
||||
{
|
||||
handler: () => {
|
||||
openWindow(VBEN_GITHUB_URL, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: MdiGithub,
|
||||
text: 'GitHub',
|
||||
},
|
||||
{
|
||||
handler: () => {
|
||||
openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: CircleHelp,
|
||||
text: $t('ui.widgets.qa'),
|
||||
},
|
||||
// TODO 后续放个人中心
|
||||
// {
|
||||
// handler: () => {
|
||||
// openWindow(VBEN_DOC_URL, {
|
||||
// target: '_blank',
|
||||
// });
|
||||
// },
|
||||
// icon: BookOpenText,
|
||||
// text: $t('ui.widgets.document'),
|
||||
// },
|
||||
// {
|
||||
// handler: () => {
|
||||
// openWindow(VBEN_GITHUB_URL, {
|
||||
// target: '_blank',
|
||||
// });
|
||||
// },
|
||||
// icon: MdiGithub,
|
||||
// text: 'GitHub',
|
||||
// },
|
||||
// {
|
||||
// handler: () => {
|
||||
// openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
||||
// target: '_blank',
|
||||
// });
|
||||
// },
|
||||
// icon: CircleHelp,
|
||||
// text: $t('ui.widgets.qa'),
|
||||
// },
|
||||
]);
|
||||
|
||||
const avatar = computed(() => {
|
||||
@@ -96,6 +98,7 @@ const avatar = computed(() => {
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout(false);
|
||||
destroyWatermark();
|
||||
}
|
||||
|
||||
function handleNoticeClear() {
|
||||
@@ -110,7 +113,9 @@ watch(
|
||||
async (enable) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: `${userStore.userInfo?.username}`,
|
||||
// 这里更改水印内容
|
||||
// content: `${userStore.userInfo?.nick_name || import.meta.env.VITE_APP_TITLE}`,
|
||||
content: `${userStore.userInfo?.nick_name}\r\n${userStore.userInfo?.phone || import.meta.env.VITE_APP_TITLE}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
@@ -127,10 +132,10 @@ watch(
|
||||
<template #user-dropdown>
|
||||
<UserDropdown
|
||||
:avatar
|
||||
:description="userStore.userInfo?.email"
|
||||
:menus
|
||||
:text="userStore.userInfo?.realName"
|
||||
description="ann.vben@gmail.com"
|
||||
tag-text="Pro"
|
||||
:tag-text="userStore.userInfo?.roles?.name"
|
||||
:text="userStore.userInfo?.nick_name"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -9,5 +9,43 @@ export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
layout: 'sidebar-mixed-nav',
|
||||
// 登录过期模式
|
||||
loginExpiredMode: 'modal',
|
||||
// 权限模式 frontend 默认前端控制 backend 后端控制
|
||||
accessMode: 'backend',
|
||||
enableRefreshToken: true,
|
||||
defaultAvatar:
|
||||
'https://yanydy.oss-cn-hangzhou.aliyuncs.com/uploads/20241219/2b38e38414a2b9383b8643983e3f69d7.png',
|
||||
watermark: true,
|
||||
// 是否开启检查更新
|
||||
enableCheckUpdates: true,
|
||||
// 检查更新的时间间隔,单位为分钟
|
||||
checkUpdatesInterval: 1,
|
||||
version: '1.0.0',
|
||||
},
|
||||
sidebar: {
|
||||
extraCollapse: false,
|
||||
},
|
||||
footer: {
|
||||
// enable: true,
|
||||
// fixed: true,
|
||||
},
|
||||
logo: {
|
||||
enable: true,
|
||||
source:
|
||||
// 'https://yanydy.oss-cn-hangzhou.aliyuncs.com/uploads/20241219/2b38e38414a2b9383b8643983e3f69d7.png',
|
||||
'/img/logo.png',
|
||||
},
|
||||
theme: {
|
||||
mode: 'auto',
|
||||
builtinType: 'sky-blue',
|
||||
colorPrimary: 'hsl(231 98% 65%)',
|
||||
},
|
||||
copyright: {
|
||||
companyName: '萧康云医',
|
||||
companySiteLink: 'https://admin.xiaokang88.com',
|
||||
icp: '浙ICP备19041368号-2',
|
||||
icpLink: 'https://beian.miit.gov.cn/#/Integrated/recordQuery',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ function setupAccessGuard(router: Router) {
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
|
||||
const userRoles = userInfo.roles ?? [];
|
||||
const userRoles = userInfo?.roles ?? [];
|
||||
|
||||
// 生成菜单和路由
|
||||
const { accessibleMenus, accessibleRoutes } = await generateAccess({
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: $t('demos.title'),
|
||||
},
|
||||
name: 'Demos',
|
||||
path: '/demos',
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.antd'),
|
||||
},
|
||||
name: 'AntDesignDemos',
|
||||
path: '/demos/ant-design',
|
||||
component: () => import('#/views/demos/antd/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
// import type { RouteRecordRaw } from 'vue-router';
|
||||
//
|
||||
// import { BasicLayout } from '#/layouts';
|
||||
// import { $t } from '#/locales';
|
||||
//
|
||||
// const routes: RouteRecordRaw[] = [
|
||||
// {
|
||||
// component: BasicLayout,
|
||||
// meta: {
|
||||
// icon: 'ic:baseline-view-in-ar',
|
||||
// keepAlive: true,
|
||||
// order: 1000,
|
||||
// title: $t('demos.title'),
|
||||
// },
|
||||
// name: 'Demos',
|
||||
// path: '/demos',
|
||||
// children: [
|
||||
// {
|
||||
// meta: {
|
||||
// title: $t('demos.antd'),
|
||||
// },
|
||||
// name: 'AntDesignDemos',
|
||||
// path: '/demos/ant-design',
|
||||
// component: () => import('#/views/demos/antd/index.vue'),
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ];
|
||||
//
|
||||
// export default routes;
|
||||
|
||||
45
apps/web-antd/src/router/routes/modules/sys.ts
Normal file
45
apps/web-antd/src/router/routes/modules/sys.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'carbon:cics-system-group',
|
||||
keepAlive: true,
|
||||
order: 1000,
|
||||
title: '基础管理',
|
||||
},
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
children: [
|
||||
{
|
||||
meta: {
|
||||
title: '管理员管理',
|
||||
},
|
||||
name: 'SystemUser',
|
||||
path: '/system/user',
|
||||
component: () => import('#/views/system/admin/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '角色管理',
|
||||
},
|
||||
name: 'SystemRole',
|
||||
path: '/system/role',
|
||||
component: () => import('#/views/system/role/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: '菜单管理',
|
||||
},
|
||||
name: 'SystemMenu',
|
||||
path: '/system/menu',
|
||||
component: () => import('#/views/system/menu/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
350
apps/web-antd/src/util/tool.ts
Normal file
350
apps/web-antd/src/util/tool.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import type { DescItem } from '#/components/description';
|
||||
|
||||
import { isFunction } from '@vben/utils';
|
||||
|
||||
export const omit = (obj: any, keysToOmit: string[]) => {
|
||||
// 如果 obj 不是对象或者 keysToOmit 不是数组,则直接返回 obj
|
||||
if (typeof obj !== 'object' || !Array.isArray(keysToOmit)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 创建一个新的对象,避免修改原始对象
|
||||
const result = {} as any;
|
||||
|
||||
// 遍历 obj 的所有键值对
|
||||
for (const key in obj) {
|
||||
// 如果当前键不在要忽略的键列表中,则复制到新对象
|
||||
if (!keysToOmit.includes(key)) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
export const get = (object: any, path: string) => {
|
||||
// 如果 object 不是对象或者 path 不是字符串,则直接返回 defaultValue
|
||||
if (
|
||||
typeof object !== 'object' ||
|
||||
object === null ||
|
||||
typeof path !== 'string'
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
|
||||
// 将路径字符串转换为数组
|
||||
const pathArray = path.split('.').filter(Boolean); // 过滤掉空字符串
|
||||
let current = object;
|
||||
|
||||
// 遍历路径数组
|
||||
for (const element of pathArray) {
|
||||
// 如果当前层级不是对象或没有对应的键,则返回 defaultValue
|
||||
if (
|
||||
typeof current !== 'object' ||
|
||||
current === null ||
|
||||
!(element in current)
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
// 更新 current 到下一层级
|
||||
current = current[element];
|
||||
}
|
||||
|
||||
// 返回最终找到的值
|
||||
return current;
|
||||
};
|
||||
export const ifDetail = (renderCallbackParams: any): boolean => {
|
||||
const schema = renderCallbackParams.schema;
|
||||
if (isFunction(schema.ifDetail)) {
|
||||
return schema.ifDetail(renderCallbackParams);
|
||||
}
|
||||
return schema.ifDetail !== false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将表单元数据转换为详情表单
|
||||
* @param formOptions
|
||||
* @param values
|
||||
*/
|
||||
export const schemaToDetailForm = (
|
||||
formOptions: VbenFormProps,
|
||||
values: any,
|
||||
): DescItem[] => {
|
||||
const group: Array<DescItem> = [];
|
||||
const formSchemas = formOptions.schema;
|
||||
if (!Array.isArray(formSchemas)) return group;
|
||||
for (let i = 0; i < formSchemas.length; i++) {
|
||||
const item = formSchemas[i];
|
||||
// 找到分割线
|
||||
if (item?.component === 'Divider') {
|
||||
group.push({
|
||||
field: item.fieldName,
|
||||
label: item.label,
|
||||
children: [],
|
||||
} as unknown as DescItem);
|
||||
// 将分割线后面的数据添加到group中
|
||||
for (let j = i + 1; j < formSchemas.length; j++) {
|
||||
const nextItem = formSchemas[j];
|
||||
// 找到下面的分割线
|
||||
if (nextItem?.component === 'Divider') {
|
||||
// 中断
|
||||
i = j - 1;
|
||||
break;
|
||||
} else {
|
||||
if (
|
||||
ifDetail({
|
||||
schema: nextItem,
|
||||
values,
|
||||
model: values,
|
||||
field: item.fieldName,
|
||||
})
|
||||
) {
|
||||
group[group.length - 1]?.children?.push({
|
||||
field: nextItem?.fieldName,
|
||||
label: nextItem?.label,
|
||||
span: nextItem?.detailSpan || 12,
|
||||
component: nextItem?.component,
|
||||
componentProps: nextItem?.componentProps,
|
||||
} as DescItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (group.length > 0) {
|
||||
return group;
|
||||
}
|
||||
return [
|
||||
{
|
||||
field: 'baseinfo',
|
||||
label: '基本信息',
|
||||
children: formSchemas
|
||||
.filter((item) => {
|
||||
return ifDetail({
|
||||
schema: item,
|
||||
values,
|
||||
model: values,
|
||||
field: item.fieldName,
|
||||
});
|
||||
})
|
||||
.map((item) => {
|
||||
return {
|
||||
field: item.fieldName,
|
||||
label: item.label,
|
||||
span: item.detailSpan || 12,
|
||||
component: item.component,
|
||||
componentProps: item.componentProps,
|
||||
} as DescItem;
|
||||
}),
|
||||
} as DescItem,
|
||||
];
|
||||
};
|
||||
|
||||
// 获取子节点id
|
||||
export const getChildIds = (record: any) => {
|
||||
const ids = [record.id];
|
||||
record.children?.forEach((item: any) => {
|
||||
ids.push(...getChildIds(item));
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有叶子节点
|
||||
* @param treeData
|
||||
*/
|
||||
export const getLeafNodeIds = (treeData: any) => {
|
||||
const leafNodeIds: any = [];
|
||||
|
||||
function traverse(node: any) {
|
||||
// 如果当前节点没有 children 或 children 是空数组,则它是叶子节点
|
||||
if (!node.children || node.children.length === 0) {
|
||||
leafNodeIds.push(node.id);
|
||||
} else {
|
||||
// 否则,递归遍历每个子节点
|
||||
for (const child of node.children) {
|
||||
traverse(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历树的根节点
|
||||
for (const root of treeData) {
|
||||
traverse(root);
|
||||
}
|
||||
return leafNodeIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有节点id
|
||||
* @param treeData
|
||||
*/
|
||||
export const getAllNodeIds = (treeData: any) => {
|
||||
const allNodeIds: any = [];
|
||||
|
||||
function traverse(node: any) {
|
||||
// 收集当前节点的 id
|
||||
allNodeIds.push(node.id);
|
||||
|
||||
// 如果当前节点有 children,则递归遍历每个子节点
|
||||
if (node.children && node.children.length > 0) {
|
||||
for (const child of node.children) {
|
||||
traverse(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历树的根节点
|
||||
for (const root of treeData) {
|
||||
traverse(root);
|
||||
}
|
||||
|
||||
return allNodeIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* -转大驼峰
|
||||
* @param str
|
||||
*/
|
||||
export const toPascalCase = (str: any) => {
|
||||
// 将连字符或下划线替换为空格,以便后续处理
|
||||
const words = str.replaceAll(/[-_]/g, ' ').split(' ');
|
||||
|
||||
// 将每个单词的首字母大写,并将其余部分保持原样
|
||||
const pascalCaseWords = words.map((word: any) => {
|
||||
if (word) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
return word;
|
||||
});
|
||||
|
||||
// 将处理后的单词拼接成一个新的字符串
|
||||
return pascalCaseWords.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* 转-kebab
|
||||
*/
|
||||
export const toKebabCase = (str: string) => {
|
||||
// 将每个单词的首字母小写,并在单词之间插入连字符
|
||||
return str
|
||||
.replaceAll(/([A-Z])/g, (match, _p1, offset) => {
|
||||
// 如果是第一个字符,直接转为小写
|
||||
if (offset === 0) {
|
||||
return match.toLowerCase();
|
||||
}
|
||||
// 否则,在大写字母前加上连字符,并将大写字母转为小写
|
||||
return `-${match.toLowerCase()}`;
|
||||
})
|
||||
.toLowerCase(); // 确保整个字符串都是小写
|
||||
};
|
||||
|
||||
export function getFileNameWithoutExtension(path: string) {
|
||||
// 使用正则表达式匹配最后一个 / 后面的所有字符,但不包括最后一个点(.)之后的内容
|
||||
const match = path.match(/[^/]+(?=\.[^./]+$|$)/);
|
||||
return match ? match[0].replace(/\.[^/.]+$/, '') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据脱敏
|
||||
* @param data
|
||||
* @param type
|
||||
*/
|
||||
export function desensitize(data: string, type: 'bankCard' | 'idCard'): string {
|
||||
if (!data) return '';
|
||||
|
||||
switch (type) {
|
||||
case 'bankCard': {
|
||||
// 银行卡号脱敏规则:显示前6位和后4位,中间用*代替
|
||||
if (data.length < 12) {
|
||||
throw new Error('Invalid bank card number length');
|
||||
}
|
||||
return data.slice(0, 6) + '*'.repeat(data.length - 10) + data.slice(-4);
|
||||
}
|
||||
|
||||
case 'idCard': {
|
||||
// 身份证号脱敏规则:显示前6位和后4位,中间用*代替
|
||||
if (data.length !== 18) {
|
||||
throw new Error('Invalid ID card number length');
|
||||
}
|
||||
return data.slice(0, 6) + '*'.repeat(10) + data.slice(-4);
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('Unsupported desensitization type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作系统的Icon
|
||||
* @param str
|
||||
*/
|
||||
export function getIcon(str: string) {
|
||||
switch (str) {
|
||||
case 'Api Post': {
|
||||
return 'logos:async-api-icon';
|
||||
}
|
||||
case 'Chrome': {
|
||||
return 'devicon:chrome';
|
||||
}
|
||||
case 'Edge':
|
||||
case 'Microsoft Edge': {
|
||||
return 'logos:microsoft-edge';
|
||||
}
|
||||
case 'Firefox': {
|
||||
return 'devicon:firefox';
|
||||
}
|
||||
case 'Linux': {
|
||||
return 'logos:linux-tux';
|
||||
}
|
||||
case 'Mac': {
|
||||
return 'wpf:mac-os';
|
||||
}
|
||||
case 'Safari': {
|
||||
return 'devicon:safari';
|
||||
}
|
||||
case 'Windows': {
|
||||
return 'logos:microsoft-windows-icon';
|
||||
}
|
||||
default: {
|
||||
return 'logos:microsoft-windows-icon';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取接诊状态
|
||||
* @param status
|
||||
*/
|
||||
export function getRegisterStatus(status: number) {
|
||||
switch (status) {
|
||||
case 0: {
|
||||
return '待支付';
|
||||
}
|
||||
case 1: {
|
||||
return '待接诊';
|
||||
}
|
||||
case 2: {
|
||||
return '接诊中';
|
||||
}
|
||||
case 3: {
|
||||
return '已结束';
|
||||
}
|
||||
case 4: {
|
||||
return '已取消';
|
||||
}
|
||||
case 5: {
|
||||
return '待评价';
|
||||
}
|
||||
case 6: {
|
||||
return '已评价';
|
||||
}
|
||||
case 7: {
|
||||
return '已拒诊';
|
||||
}
|
||||
default: {
|
||||
return '未知状态';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
return countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed, markRaw, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, type VbenFormSchema } from '@vben/common-ui';
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { sendVerificationCode } from '#/api';
|
||||
import { useAuthStore } from '#/store';
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const loginRef =
|
||||
useTemplateRef<InstanceType<typeof AuthenticationCodeLogin>>('loginRef');
|
||||
const authStore = useAuthStore();
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const loading = ref(false);
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
@@ -30,7 +36,7 @@ const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
/* {
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
@@ -43,7 +49,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('vben'),
|
||||
},
|
||||
},*/
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
@@ -57,8 +63,8 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: '123456',
|
||||
username: findUser.value,
|
||||
password: 'qiqi991012',
|
||||
username: '15100000000',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -67,7 +73,12 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
@@ -77,6 +88,53 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
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',
|
||||
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);
|
||||
loading.value = false;
|
||||
message.success('发送成功,请注意查收');
|
||||
},
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: markRaw(SliderCaptcha),
|
||||
@@ -91,8 +149,15 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
:show-code-login="false"
|
||||
:show-forget-password="false"
|
||||
:show-qrcode-login="false"
|
||||
:show-register="false"
|
||||
:show-remember-me="false"
|
||||
:show-third-party-login="false"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
const prefix = 'express-companies/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getExpressCompaniesList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getExpressCompaniesOption(data: any) {
|
||||
return requestClient.get<any>(`${prefix}option`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取快递公司详情
|
||||
* @param id
|
||||
*/
|
||||
export async function getExpressCompaniesInfo(id: number) {
|
||||
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增快递公司
|
||||
* @param data
|
||||
*/
|
||||
export async function createExpressCompanies(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑快递公司
|
||||
* @param data
|
||||
*/
|
||||
export async function updateExpressCompanies(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除快递公司
|
||||
* @param data
|
||||
*/
|
||||
export async function deleteExpressCompanies(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { createExpressCompanies, updateExpressCompanies } from '../api';
|
||||
import { modalFormProps } from '../config/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = isUpdate.value
|
||||
? updateExpressCompanies
|
||||
: createExpressCompanies;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
isUpdate.value = update;
|
||||
formApi.setValues(values);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}快递公司`" class="w-[30%]">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
export const modalFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// handleSubmit: onSubmit,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
formItemClass: 'col-span-6',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入快递公司昵称',
|
||||
},
|
||||
fieldName: 'name',
|
||||
label: '快递公司名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入快递公司编码',
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: '快递公司编码',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入快递公司类型',
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '快递公司类型',
|
||||
rules: 'required',
|
||||
},
|
||||
// {
|
||||
// component: 'Avatar',
|
||||
// fieldName: 'logo',
|
||||
// label: 'LOGO',
|
||||
// rules: 'required',
|
||||
// },
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入快递公司介绍',
|
||||
},
|
||||
fieldName: 'introduce',
|
||||
label: '快递公司介绍',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '输入名称',
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'name',
|
||||
label: '快递公司名称',
|
||||
},
|
||||
// {
|
||||
// component: 'RangePicker',
|
||||
// componentProps: {
|
||||
// format: 'YYYY-MM-DD', // 确保格式与你需要的一致
|
||||
// },
|
||||
// // defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
// fieldName: 'search_time',
|
||||
// label: '时间范围',
|
||||
// },
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getExpressCompaniesList } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
introduce: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'name', align: 'left', title: '快递公司每次' },
|
||||
{ field: 'code', title: '快递公司编码' },
|
||||
{ field: 'type', title: '快递公司类型' },
|
||||
{ field: 'created_at', title: '上传时间' },
|
||||
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getExpressCompaniesList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import { deleteExpressCompanies } from './api';
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const showModal = (data = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
|
||||
const deleteApi = (row: any) => {
|
||||
let ids = [];
|
||||
if (row) {
|
||||
ids.push(row);
|
||||
} else {
|
||||
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
|
||||
}
|
||||
deleteExpressCompanies({ ids }).then(() => {
|
||||
message.success('删除成功!');
|
||||
gridApi.reload();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="快递公司管理">
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
type: 'primary',
|
||||
icon: 'ant-design:plus-outlined',
|
||||
// auth: ['超级快递公司', 'sys:user:save'],
|
||||
onClick: showModal.bind(null),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'ant-design:delete-outlined',
|
||||
ifShow: hasTopTableDropDownActions,
|
||||
// auth: ['超级快递公司', 'sys:user:save'],
|
||||
popConfirm: {
|
||||
title: '确定删除吗?',
|
||||
confirm: deleteApi.bind(null, false),
|
||||
},
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #more>
|
||||
<Button style="margin-left: 16px">
|
||||
批量操作
|
||||
<Icon icon="ant-design:down-outlined" />
|
||||
</Button>
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #logo="{ row }">
|
||||
<Image :src="row.logo" height="30" width="30" />
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
// auth: ['express-companies', 'sys:role:detail'],
|
||||
onClick: showModal.bind(null, row, true),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
icon: 'ant-design:delete-outlined',
|
||||
size: 'small',
|
||||
// auth: ['express-companies', 'sys:role:detail'],
|
||||
popConfirm: {
|
||||
title: '确定删除吗?',
|
||||
confirm: deleteApi.bind(null, row.id),
|
||||
},
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
// {
|
||||
// label: '编辑',
|
||||
// type: 'link',
|
||||
// icon: 'ant-design:delete-outlined',
|
||||
// size: 'small',
|
||||
// // auth: ['express-companies', 'sys:role:detail'],
|
||||
// onClick: showModal.bind(null, row, true),
|
||||
// },
|
||||
// {
|
||||
// label: '删除',
|
||||
// type: 'link',
|
||||
// icon: 'ant-design:delete-outlined',
|
||||
// size: 'small',
|
||||
// // auth: ['express-companies', 'sys:role:detail'],
|
||||
// popConfirm: {
|
||||
// title: '确定删除吗?',
|
||||
// confirm: deleteApi.bind(null, row.id),
|
||||
// },
|
||||
// },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'order/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getOrderList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getOrderStatusOption(data: any) {
|
||||
return requestClient.get<any>(`${prefix}status-option`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
* @param id
|
||||
*/
|
||||
export async function getOrderInfo(id: number) {
|
||||
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增订单
|
||||
* @param data
|
||||
*/
|
||||
export async function createOrder(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑订单
|
||||
* @param data
|
||||
*/
|
||||
export async function updateOrder(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除订单
|
||||
* @param data
|
||||
*/
|
||||
export async function deleteOrder(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单发货
|
||||
* @param data
|
||||
*/
|
||||
export async function sendOrder(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}ware-send`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单物流追踪
|
||||
* @param data
|
||||
*/
|
||||
export async function expressDetailByOrderId(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`express-detail/detail-by-order`, data);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Card, Descriptions, Image, Tag, Timeline } from 'ant-design-vue';
|
||||
|
||||
import { expressDetailByOrderId } from '../api';
|
||||
|
||||
defineOptions({
|
||||
name: 'DetailModal',
|
||||
});
|
||||
|
||||
const gridApi = ref();
|
||||
// 订单信息
|
||||
const data = ref({});
|
||||
// 快递信息
|
||||
const expressDetail = ref({});
|
||||
// 订单发货方式
|
||||
const deliveryMethod = ref(-1);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
data.value = values;
|
||||
deliveryMethod.value = data.value.delivery_method;
|
||||
getExpressDetail();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function getExpressDetail() {
|
||||
expressDetail.value = await expressDetailByOrderId({
|
||||
order_id: data.value.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物流信息标签颜色
|
||||
* @param status
|
||||
*/
|
||||
function getColor(status: any) {
|
||||
switch (status) {
|
||||
case '在途': {
|
||||
return '';
|
||||
}
|
||||
case '揽收': {
|
||||
return 'orange';
|
||||
}
|
||||
case '派件': {
|
||||
return 'blue';
|
||||
}
|
||||
case '签收': {
|
||||
return 'green';
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-[80%]" title="订单详情">
|
||||
<div class="flex flex-col gap-4">
|
||||
<h3 class="mt-4">订单信息</h3>
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
class="mt-4"
|
||||
>
|
||||
<Descriptions.Item label="订单号">
|
||||
{{ data.order_no }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="订单类型">
|
||||
{{ data.order_type === 1 ? '普通订单' : '其他' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="总支付价格">
|
||||
{{ data.total_pay_price }} 元
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="物品总价">
|
||||
{{ data.items_price }} 元
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="市场价">
|
||||
{{ data.market_price }} 元
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运费">
|
||||
{{ data.trans_expenses }} 元
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="是否免邮">
|
||||
{{ data.free_ship === 1 ? '是' : '否' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{{ data.status === 0 ? '待处理' : '已处理' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="配送方式">
|
||||
{{ deliveryMethod === 0 ? '快递到家' : '药店自提' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="患者">{{ data.patient }}</Descriptions.Item>
|
||||
<Descriptions.Item label="医生">
|
||||
{{ data.doctor.name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="处方来源">
|
||||
{{ data.store.name }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div class="mt-4">
|
||||
<h3>{{ deliveryMethod === 0 ? '收货人' : '就诊人' }}信息</h3>
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
class="mt-4"
|
||||
>
|
||||
<Descriptions.Item
|
||||
:label="deliveryMethod === 0 ? '收货人' : '就诊人'"
|
||||
>
|
||||
<span v-if="deliveryMethod === 0">{{ data.address?.name }}</span>
|
||||
<span v-else>
|
||||
{{
|
||||
data.express_name || data.patient?.name || data.cancel_remark
|
||||
}}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="联系电话">
|
||||
{{ data.address?.mobile || data.patient_mobile }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="deliveryMethod === 0" label="配送地址">
|
||||
{{
|
||||
`${data.address?.province || ''} ${data.address?.region || ''} ${data.address?.detail_address || ''}`
|
||||
}}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-else label="自提地址">
|
||||
{{ `${data.store?.position || ''} ` }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="发货备注">
|
||||
{{ data.cancel_remark || '无' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3>订单商品</h3>
|
||||
<Card
|
||||
v-for="(item, index) in data.product_order_items"
|
||||
:key="index"
|
||||
class="mb-4 mt-5"
|
||||
>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div
|
||||
class="mr-6 h-32 w-32 overflow-hidden rounded-lg bg-gray-200 dark:bg-gray-800"
|
||||
>
|
||||
<Image :src="item.drug_image" alt="" height="100%" width="100%" />
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ item.drug_name }}</h4>
|
||||
<p>规格: {{ item.drug.specification }}</p>
|
||||
<p>数量: {{ item.number }}</p>
|
||||
<p>单价: {{ item.price }} 元</p>
|
||||
<p>
|
||||
厂家:
|
||||
{{
|
||||
item.drug
|
||||
? item.drug.source_info != null
|
||||
? item.drug.source_info.name
|
||||
: item.drug.source
|
||||
: '暂无'
|
||||
}}
|
||||
</p>
|
||||
<!-- <p>总价: {{ item.total_price }} 元</p>-->
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3>处方信息</h3>
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
class="mt-4"
|
||||
>
|
||||
<Descriptions.Item label="处方编号">
|
||||
{{ data.prescription.content.prescription_no }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="诊断">
|
||||
{{ data.prescription.content.clinical_diagnose }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="医嘱">
|
||||
{{ data.prescription.content.doctor_order }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开药医生">
|
||||
{{ data.prescription.content.doctor.name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="科室">
|
||||
{{ data.prescription.content.doctor.depart.name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="职称">
|
||||
{{ data.prescription.content.doctor.title.name }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
<div v-if="deliveryMethod === 0">
|
||||
<div class="mt-4">
|
||||
<h3>物流信息</h3>
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
class="mt-4"
|
||||
>
|
||||
<Descriptions.Item label="物流公司">
|
||||
{{ expressDetail.express_company_name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运单号">
|
||||
{{ expressDetail.express_no }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最新状态">
|
||||
{{ expressDetail.state_txt }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3>物流追踪</h3>
|
||||
<Timeline class="mt-4">
|
||||
<Timeline.Item
|
||||
v-for="(detail, index) in expressDetail.detail"
|
||||
:key="index"
|
||||
>
|
||||
<Tag :color="getColor(detail.status)">
|
||||
{{ detail.status }}
|
||||
</Tag>
|
||||
<p>{{ detail.detail_at }}</p>
|
||||
<p>{{ detail.detail }}</p>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ant-descriptions-item-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.space-x-4 > * + * {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { sendOrder } from '#/views/business/order/product-order/api';
|
||||
import { modalFormProps } from '#/views/business/order/product-order/config/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
const orderNo = ref('');
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = sendOrder;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('发货成功');
|
||||
gridApi.value?.reload();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
orderNo.value = values.order_no;
|
||||
isUpdate.value = update;
|
||||
formApi.setValues(values);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal :title="`订单:【 ${orderNo} 】发货`" class="w-[30%]">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { getExpressCompaniesOption } from '#/views/business/express/express-company/api';
|
||||
|
||||
export const modalFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// handleSubmit: onSubmit,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'order_id',
|
||||
label: 'ID',
|
||||
formItemClass: 'col-span-6',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: ['oreder_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入快递单号',
|
||||
},
|
||||
fieldName: 'express_no',
|
||||
label: '快递单号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
// filterOption: true,
|
||||
showSearch: true,
|
||||
filterOption: (input: string, option: any) => {
|
||||
// 自定义过滤逻辑,确保可以根据 name 进行搜索
|
||||
return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
},
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { id: number; name: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.code,
|
||||
}));
|
||||
},
|
||||
api: getExpressCompaniesOption,
|
||||
placeholder: '请选择',
|
||||
},
|
||||
fieldName: 'express_company_code',
|
||||
formItemClass: 'col-span-6',
|
||||
label: '快递公司',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入订单备注',
|
||||
},
|
||||
fieldName: 'introduce',
|
||||
label: '发货备注',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import {getOrderStatusOption} from "#/views/business/order/product-order/api";
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '输入订单号',
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'order_no',
|
||||
label: '订单号',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
// showSearch: true,
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { id: number; name: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
},
|
||||
api: getOrderStatusOption,
|
||||
placeholder: '请选择',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '订单状态',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
showSearch: true,
|
||||
options: [
|
||||
{
|
||||
label: '快递到家',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '诊所自提',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
},
|
||||
fieldName: 'delivery_method',
|
||||
label: '发货方式',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
showSearch: true,
|
||||
options: [
|
||||
{
|
||||
label: '中药',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '西药',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '中成药',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '产品服务包',
|
||||
value: 5,
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
},
|
||||
fieldName: 'prescription_type',
|
||||
label: '订单类型',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD', // 确保格式与你需要的一致
|
||||
},
|
||||
defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
fieldName: 'search_time',
|
||||
label: '时间范围',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
// submitOnChange: true,
|
||||
// submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getOrderList } from '#/views/business/order/product-order/api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
introduce: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columns: [
|
||||
// { type: 'checkbox', width: 60 },
|
||||
{ type: 'expand', width: 80, slots: { content: 'expand-content' } },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'order_no', align: 'left', title: '订单号' },
|
||||
{
|
||||
field: 'user.avatarUrl',
|
||||
title: '下单用户信息',
|
||||
slots: { default: 'avatar' },
|
||||
width: 100,
|
||||
},
|
||||
{ field: 'store.name', title: '诊所名称' },
|
||||
{
|
||||
field: 'address.name',
|
||||
title: '收货人姓名',
|
||||
slots: { default: 'address-name' },
|
||||
},
|
||||
{
|
||||
field: 'address.mobile',
|
||||
title: '收货人联系方式',
|
||||
slots: { default: 'address-mobile' },
|
||||
},
|
||||
{
|
||||
field: 'delivery_method',
|
||||
title: '订单类型&邮寄方式&订单状态',
|
||||
slots: { default: 'delivery-method' },
|
||||
},
|
||||
{ field: 'items_price', title: '总价' },
|
||||
{ field: 'pay_time', title: '支付时间', slots: { default: 'pay-time' } },
|
||||
{ field: 'created_at', title: '下单时间' },
|
||||
{
|
||||
type: 'html',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
slots: { default: 'action' },
|
||||
width: 200,
|
||||
},
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
// scrollY: {
|
||||
// enabled: true,
|
||||
// gt: 0,
|
||||
// },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOrderList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
expandConfig: {
|
||||
// expandAll: true,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
315
apps/web-antd/src/views/business/order/product-order/index.vue
Normal file
315
apps/web-antd/src/views/business/order/product-order/index.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import DetailModal from './components/detail.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: DetailModal,
|
||||
});
|
||||
|
||||
const infoModal = (data = {}) => {
|
||||
modalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
gridApi,
|
||||
});
|
||||
modalApi.open();
|
||||
// message.success(`正在开发${JSON.stringify(data)}`);
|
||||
};
|
||||
// const showModal = (data = {}, isUpdate = false) => {
|
||||
// formModalApi.setData({
|
||||
// // 表单值
|
||||
// values: data,
|
||||
// update: isUpdate,
|
||||
// gridApi,
|
||||
// });
|
||||
// formModalApi.open();
|
||||
// };
|
||||
|
||||
const wareSend = (data = {}) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: {
|
||||
order_id: data?.id,
|
||||
order_no: data?.order_no,
|
||||
},
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
const expandAll = () => {
|
||||
gridApi.grid?.setAllRowExpand(true);
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
gridApi.grid?.setAllRowExpand(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="订单管理">
|
||||
<FormModal />
|
||||
<Modal />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '展开全部',
|
||||
type: 'primary',
|
||||
// auth: ['超级菜单', 'sys:user:save'],
|
||||
onClick: expandAll.bind(null),
|
||||
},
|
||||
{
|
||||
label: '收起全部',
|
||||
type: 'primary',
|
||||
// auth: ['超级菜单', 'sys:user:save'],
|
||||
onClick: collapseAll.bind(null),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
>
|
||||
<template #more>
|
||||
<Button style="margin-left: 16px">
|
||||
批量操作
|
||||
<Icon icon="ant-design:down-outlined" />
|
||||
</Button>
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #avatar="{ row }">
|
||||
<Image :src="row.user.avatarurl || '/img/user-default-avatar.png'" />
|
||||
{{ row.user.nickname }}
|
||||
</template>
|
||||
<template #address-name="{ row }">
|
||||
{{
|
||||
row.address?.name ||
|
||||
row.express_name ||
|
||||
row.patient?.name ||
|
||||
row.cancel_remark
|
||||
}}
|
||||
</template>
|
||||
<template #address-mobile="{ row }">
|
||||
{{ row.address?.mobile || row.patient_mobile }}
|
||||
</template>
|
||||
<template #pay-time="{ row }">
|
||||
{{ row?.pay_time || '未支付' }}
|
||||
</template>
|
||||
<template #delivery-method="{ row }">
|
||||
<div v-if="row.order_type === 1">
|
||||
<Tag v-if="row.prescription_type === 1" color="orange">中药订单</Tag>
|
||||
<Tag v-else-if="row.prescription_type === 2" color="blue">
|
||||
商品订单【西药】
|
||||
</Tag>
|
||||
<Tag v-else-if="row.prescription_type === 3" color="purple">
|
||||
商品订单【非处方药】
|
||||
</Tag>
|
||||
<Tag v-else-if="row.prescription_type === 4" color="green">
|
||||
已退款
|
||||
</Tag>
|
||||
<Tag v-else-if="row.prescription_type === 5" color="pink">
|
||||
产品服务包
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Tag v-if="row.delivery_method === 1" color="blue">【到店自提】</Tag>
|
||||
<Tag v-else-if="row.delivery_method === 0" color="green">
|
||||
【上门快递】
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Tag v-if="row.status === 0" color="red">待支付</Tag>
|
||||
<Tag v-else-if="row.status === 1" color="orange">待发货</Tag>
|
||||
<Tag v-else-if="row.status === 2" color="blue">待收货</Tag>
|
||||
<Tag v-else-if="row.status === 3" color="purple">待评价</Tag>
|
||||
<Tag v-else-if="row.status === 4" color="green">已退款</Tag>
|
||||
<Tag v-else-if="row.status === 5" color="pink">退款中</Tag>
|
||||
<Tag v-else-if="row.status === 6" color="cyan">已收货</Tag>
|
||||
<Tag v-else-if="row.status === 7" color="green">确认收货</Tag>
|
||||
<Tag v-else-if="row.status === 8" color="#B22222">拒绝退款</Tag>
|
||||
<Tag v-else-if="row.status === 9" color="gray">已取消</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '详情',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
// auth: ['order', 'sys:role:detail'],
|
||||
onClick: infoModal.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '发货',
|
||||
type: 'link',
|
||||
icon: 'ri:send-plane-fill',
|
||||
// auth: ['超级订单', 'sys:user:save'],
|
||||
onClick: wareSend.bind(null, row),
|
||||
// popConfirm: {
|
||||
// title: '确定发货吗?',
|
||||
// confirm: wareSend.bind(null, row),
|
||||
// },
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
// {
|
||||
// label: '发货',
|
||||
// type: 'link',
|
||||
// icon: 'ri:send-plane-fill',
|
||||
// // auth: ['超级订单', 'sys:user:save'],
|
||||
// popConfirm: {
|
||||
// title: '确定发货吗?',
|
||||
// confirm: wareSend.bind(null, false),
|
||||
// },
|
||||
// },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #expand-content="{ row }">
|
||||
<div
|
||||
v-if="row.prescription_type === 2 || row.prescription_type === 5"
|
||||
class="product-list mt-5"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in row.product_order_items"
|
||||
:key="index"
|
||||
class="box-card mb-6 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<div>
|
||||
<div
|
||||
class="mr-6 h-32 w-32 overflow-hidden rounded-lg bg-gray-200 dark:bg-gray-800"
|
||||
>
|
||||
<Image
|
||||
:src="item.drug_image"
|
||||
alt=""
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 mt-5 text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">商品编号:</span>
|
||||
X00{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow space-y-2">
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">商品名称:</span>
|
||||
{{ item.drug_name }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">商品规格:</span>
|
||||
{{ item.drug ? item.drug.specification : '暂无' }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">商品数量:</span>
|
||||
{{ item.number }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">商品厂家:</span>
|
||||
{{ item.drug ? item.drug.source_info != null ? item.drug.source_info.name : item.drug.source : '暂无' }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">商品价格:</span>
|
||||
{{ item.price }} 元
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="row.prescription_type === 1" class="medication-details mt-5 p-10">
|
||||
<span class="mb-2 font-medium">药品明细:</span>
|
||||
<ul class="custom-list pl-6">
|
||||
<li
|
||||
v-for="(item, index) in row.product_order_items"
|
||||
:key="index"
|
||||
class="custom-list-item"
|
||||
>
|
||||
【{{ item.drug.drug_number }}】 {{ item.drug_name }} (* {{item.number}})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="!row.prescription_type"></div>
|
||||
<div
|
||||
class="order-summary mt-6 flex justify-center space-x-4 rounded-lg bg-gray-100 p-4 dark:bg-gray-800"
|
||||
>
|
||||
<span class="text-sm">采购价:{{ row.market_price || '0.00' }}</span>
|
||||
<span class="text-sm">快递费:{{ row.trans_expenses || '0.00' }}</span>
|
||||
<span class="text-sm">诊疗费:{{ row.treatement_price || '0.00' }}</span>
|
||||
<span class="text-sm">加工费:{{ row.process_price || '0.00' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.custom-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.custom-list-item {
|
||||
background-color: rgba(64, 158, 255, 0.04);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #409eff;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'service-pack/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getServicePackList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getServicePackOption(data: any) {
|
||||
return requestClient.get<any>(`${prefix}option`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品服务包详情
|
||||
* @param id
|
||||
*/
|
||||
export async function getServicePackInfo(id: number) {
|
||||
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增产品服务包
|
||||
* @param data
|
||||
*/
|
||||
export async function createServicePack(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑产品服务包
|
||||
* @param data
|
||||
*/
|
||||
export async function updateServicePack(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除产品服务包
|
||||
* @param data
|
||||
*/
|
||||
export async function deleteServicePack(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { createServicePack, updateServicePack } from '../api';
|
||||
import { modalFormProps } from '../config/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = isUpdate.value
|
||||
? updateServicePack
|
||||
: createServicePack;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
isUpdate.value = update;
|
||||
formApi.setValues(values);
|
||||
if (userStore?.userInfo?.roles?.user_type === 3 && !isUpdate.value) {
|
||||
formApi.setValues({
|
||||
supplier_id: userStore?.userInfo?.supplier_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal
|
||||
:title="`${isUpdate === true ? '编辑' : '新增'}产品服务包`"
|
||||
class="w-[30%]"
|
||||
>
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,243 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { getSupplierOption } from '#/views/system/supplier/api';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
export const modalFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// handleSubmit: onSubmit,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
formItemClass: 'col-span-6',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'showSelect',
|
||||
label: 'ID',
|
||||
formItemClass: 'col-span-6',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Avatar',
|
||||
fieldName: 'image',
|
||||
label: '产品图片',
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-6',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
filterOption: (input: string, option: any) => {
|
||||
// 自定义过滤逻辑,确保可以根据 name 进行搜索
|
||||
return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
},
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { id: number; name: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
},
|
||||
api: getSupplierOption,
|
||||
placeholder: '请选择',
|
||||
},
|
||||
dependencies: {
|
||||
disabled() {
|
||||
return userStore?.userInfo?.roles?.user_type === 3;
|
||||
},
|
||||
triggerFields: ['supplier_id'],
|
||||
},
|
||||
fieldName: 'supplier_id',
|
||||
formItemClass: 'col-span-6',
|
||||
label: '所属供应商',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入服务包昵称',
|
||||
},
|
||||
fieldName: 'drug_name',
|
||||
formItemClass: 'col-span-6',
|
||||
label: '服务包昵称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入服务包别名',
|
||||
},
|
||||
fieldName: 'drug_alias',
|
||||
formItemClass: 'col-span-6',
|
||||
label: '服务包别名',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入服务包编号',
|
||||
},
|
||||
fieldName: 'drug_number',
|
||||
formItemClass: 'col-span-6',
|
||||
label: '服务包编号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入功能',
|
||||
},
|
||||
fieldName: 'function',
|
||||
formItemClass: 'col-span-6',
|
||||
label: '功能',
|
||||
rules: 'required',
|
||||
},
|
||||
// {
|
||||
// component: 'VbenInput',
|
||||
// componentProps: {
|
||||
// placeholder: '请输入规格',
|
||||
// },
|
||||
// fieldName: 'specification',
|
||||
// formItemClass: 'col-span-6',
|
||||
// label: '规格',
|
||||
// rules: 'required',
|
||||
// },
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
placeholder: '请选择规格',
|
||||
options: [
|
||||
{
|
||||
label: '7天',
|
||||
value: '7天',
|
||||
},
|
||||
{
|
||||
label: '14天',
|
||||
value: '14天',
|
||||
},
|
||||
{
|
||||
label: '30天',
|
||||
value: '30天',
|
||||
},
|
||||
{
|
||||
label: '60天',
|
||||
value: '60天',
|
||||
},
|
||||
{
|
||||
label: '90天',
|
||||
value: '90天',
|
||||
},
|
||||
{
|
||||
label: '180天',
|
||||
value: '180天',
|
||||
},
|
||||
],
|
||||
},
|
||||
fieldName: 'specification',
|
||||
formItemClass: 'col-span-6',
|
||||
label: '规格',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
placeholder: '请选择',
|
||||
options: [
|
||||
{
|
||||
label: '草稿',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '下架',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '上架',
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: 1,
|
||||
formItemClass: 'col-span-6',
|
||||
fieldName: 'status',
|
||||
label: '商品状态',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入平台分成',
|
||||
},
|
||||
suffix: () => '%',
|
||||
defaultValue: 15,
|
||||
fieldName: 'platform_ledger',
|
||||
formItemClass: 'col-span-4',
|
||||
label: '平台分成',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入诊所分成',
|
||||
},
|
||||
suffix: () => '%',
|
||||
defaultValue: 35,
|
||||
fieldName: 'store_ledger',
|
||||
formItemClass: 'col-span-4',
|
||||
label: '诊所分成',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入供应商分成',
|
||||
},
|
||||
suffix: () => '%',
|
||||
defaultValue: 50,
|
||||
fieldName: 'supplier_ledger',
|
||||
formItemClass: 'col-span-4',
|
||||
label: '供应商分成',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入服务包介绍',
|
||||
},
|
||||
fieldName: 'small_info',
|
||||
label: '服务包介绍',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Avatar',
|
||||
fieldName: 'instruction',
|
||||
label: '产品说明书',
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-6',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '输入名称',
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'name',
|
||||
label: '产品服务包名称',
|
||||
},
|
||||
// {
|
||||
// component: 'RangePicker',
|
||||
// componentProps: {
|
||||
// format: 'YYYY-MM-DD', // 确保格式与你需要的一致
|
||||
// },
|
||||
// // defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
// fieldName: 'search_time',
|
||||
// label: '时间范围',
|
||||
// },
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getServicePackList } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
introduce: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'drug_name', align: 'left', title: '服务包名称' },
|
||||
{ field: 'pinyin_simple', title: '拼音' },
|
||||
{ field: 'supplier.name', title: '供应商' },
|
||||
{
|
||||
field: 'image',
|
||||
align: 'left',
|
||||
title: '产品图片',
|
||||
slots: { default: 'image' },
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
field: 'instruction',
|
||||
align: 'left',
|
||||
title: '说明书',
|
||||
slots: { default: 'instruction' },
|
||||
width: 130,
|
||||
},
|
||||
{ field: 'function', title: '主要功能' },
|
||||
{ field: 'specification', title: '规格' },
|
||||
{ field: 'status', title: '状态', slots: { default: 'status' } },
|
||||
{ field: 'created_at', title: '发布时间' },
|
||||
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getServicePackList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
163
apps/web-antd/src/views/business/product/service-pack/index.vue
Normal file
163
apps/web-antd/src/views/business/product/service-pack/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import { deleteServicePack } from './api';
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const records = gridApi.grid.getCheckboxRecords();
|
||||
hasTopTableDropDownActions.value = records.length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const showModal = (data = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
|
||||
const deleteApi = (row: any) => {
|
||||
let ids = [];
|
||||
if (row) {
|
||||
ids.push(row);
|
||||
} else {
|
||||
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
|
||||
}
|
||||
deleteServicePack({ ids }).then(() => {
|
||||
message.success('删除成功!');
|
||||
gridApi.reload();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="产品服务包管理">
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
type: 'primary',
|
||||
icon: 'ant-design:plus-outlined',
|
||||
// auth: ['超级产品服务包', 'sys:user:save'],
|
||||
onClick: showModal.bind(null),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'ant-design:delete-outlined',
|
||||
ifShow: hasTopTableDropDownActions,
|
||||
// auth: ['超级产品服务包', 'sys:user:save'],
|
||||
popConfirm: {
|
||||
title: '确定删除吗?',
|
||||
confirm: deleteApi.bind(null, false),
|
||||
},
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template #more>
|
||||
<Button style="margin-left: 16px">
|
||||
批量操作
|
||||
<Icon icon="ant-design:down-outlined" />
|
||||
</Button>
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #image="{ row }">
|
||||
<Image :src="row.image" height="30" width="30" />
|
||||
</template>
|
||||
<template #instruction="{ row }">
|
||||
<div class="div" style="width: 120px;height: 120px; overflow: scroll">
|
||||
<Image :src="row.instruction" height="30" width="30" />
|
||||
</div>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
// auth: ['service-pack', 'sys:role:detail'],
|
||||
onClick: showModal.bind(null, row, true),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
icon: 'ant-design:delete-outlined',
|
||||
size: 'small',
|
||||
// auth: ['service-pack', 'sys:role:detail'],
|
||||
popConfirm: {
|
||||
title: '确定删除吗?',
|
||||
confirm: deleteApi.bind(null, row.id),
|
||||
},
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
// {
|
||||
// label: '编辑',
|
||||
// type: 'link',
|
||||
// icon: 'ant-design:delete-outlined',
|
||||
// size: 'small',
|
||||
// // auth: ['service-pack', 'sys:role:detail'],
|
||||
// onClick: showModal.bind(null, row, true),
|
||||
// },
|
||||
// {
|
||||
// label: '删除',
|
||||
// type: 'link',
|
||||
// icon: 'ant-design:delete-outlined',
|
||||
// size: 'small',
|
||||
// // auth: ['service-pack', 'sys:role:detail'],
|
||||
// popConfirm: {
|
||||
// title: '确定删除吗?',
|
||||
// confirm: deleteApi.bind(null, row.id),
|
||||
// },
|
||||
// },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { LogoSvgICON } from '@vben/icons';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
@@ -30,59 +31,77 @@ const userStore = useUserStore();
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
color: 'blue',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
group: '萧康云医科技',
|
||||
icon: LogoSvgICON,
|
||||
title: '萧康后台管理系统-旧版本',
|
||||
url: 'https://admin.xiaokang88.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
color: 'blue',
|
||||
content: '数据可视化平台。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
group: '萧康云医科技',
|
||||
icon: 'fluent-color:data-bar-vertical-ascending-20',
|
||||
title: '数据大屏',
|
||||
url: 'http://dls.xiaokang88.com',
|
||||
},
|
||||
// {
|
||||
// color: '',
|
||||
// content: '不要等待机会,而要创造机会。',
|
||||
// date: '2021-04-01',
|
||||
// group: '开源组',
|
||||
// icon: 'carbon:logo-github',
|
||||
// title: 'Github',
|
||||
// url: 'https://github.com',
|
||||
// },
|
||||
// {
|
||||
// color: '#3fb27f',
|
||||
// content: '现在的你决定将来的你。',
|
||||
// date: '2021-04-01',
|
||||
// group: '算法组',
|
||||
// icon: 'ion:logo-vue',
|
||||
// title: 'Vue',
|
||||
// url: 'https://vuejs.org',
|
||||
// },
|
||||
// {
|
||||
// color: '#e18525',
|
||||
// content: '没有什么才能比努力更重要。',
|
||||
// date: '2021-04-01',
|
||||
// group: '上班摸鱼',
|
||||
// icon: 'ion:logo-html5',
|
||||
// title: 'Html5',
|
||||
// url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
// },
|
||||
// {
|
||||
// color: '#bf0c2c',
|
||||
// content: '热情和欲望可以突破一切难关。',
|
||||
// date: '2021-04-01',
|
||||
// group: 'UI',
|
||||
// icon: 'ion:logo-angular',
|
||||
// title: 'Angular',
|
||||
// url: 'https://angular.io',
|
||||
// },
|
||||
// {
|
||||
// color: '#00d8ff',
|
||||
// content: '健康的身体是实现目标的基石。',
|
||||
// date: '2021-04-01',
|
||||
// group: '技术牛',
|
||||
// icon: 'bx:bxl-react',
|
||||
// title: 'React',
|
||||
// url: 'https://reactjs.org',
|
||||
// },
|
||||
// {
|
||||
// color: '#EBD94E',
|
||||
// content: '路是走出来的,而不是空想出来的。',
|
||||
// date: '2021-04-01',
|
||||
// group: '架构组',
|
||||
// icon: 'ion:logo-javascript',
|
||||
// title: 'Js',
|
||||
// url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
// },
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
@@ -102,26 +121,26 @@ const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
title: '管理员管理',
|
||||
url: '/system/user',
|
||||
},
|
||||
// {
|
||||
// color: '#3fb27f',
|
||||
// icon: 'ion:settings-outline',
|
||||
// title: '系统管理',
|
||||
// url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
// },
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
title: '平台管理',
|
||||
url: '/platform',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
title: '供应商管理',
|
||||
url: '/supplier',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -159,59 +178,59 @@ const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
avatar: 'svg:logo',
|
||||
content: `萧康云医上线啦!`,
|
||||
date: '2025-1-7',
|
||||
title: '萧康云医',
|
||||
},
|
||||
// {
|
||||
// avatar: 'svg:avatar-2',
|
||||
// content: `关注了 <a>威廉</a> `,
|
||||
// date: '1个小时前',
|
||||
// title: '艾文',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'svg:avatar-3',
|
||||
// content: `发布了 <a>个人动态</a> `,
|
||||
// date: '1天前',
|
||||
// title: '克里斯',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'svg:avatar-4',
|
||||
// content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
// date: '2天前',
|
||||
// title: 'Vben',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'svg:avatar-1',
|
||||
// content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
// date: '3天前',
|
||||
// title: '皮特',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'svg:avatar-2',
|
||||
// content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
// date: '1周前',
|
||||
// title: '杰克',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'svg:avatar-3',
|
||||
// content: `发布了 <a>个人动态</a> `,
|
||||
// date: '1周前',
|
||||
// title: '威廉',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'svg:avatar-4',
|
||||
// content: `推送了代码到 <a>Github</a>`,
|
||||
// date: '2021-04-01 20:00',
|
||||
// title: '威廉',
|
||||
// },
|
||||
// {
|
||||
// avatar: 'svg:avatar-4',
|
||||
// content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
// date: '2021-03-01 20:00',
|
||||
// title: 'Vben',
|
||||
// },
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
@@ -231,6 +250,20 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当天的日期和天气
|
||||
function getTodayDate() {
|
||||
const today = new Date();
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
};
|
||||
return today.toLocaleDateString('zh-CN', options);
|
||||
}
|
||||
const todayDate = getTodayDate();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -239,9 +272,15 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
哈喽, {{ userStore.userInfo?.nick_name }} -
|
||||
{{ userStore.userInfo?.roles?.name }}, 开始您一天的工作吧!
|
||||
<!-- <Button type="primary" v-access:code="['Super Admin']">测我是超级管理员</Button>-->
|
||||
<!-- <Button type="primary" v-access:code="['Admin']">测我是管理员</Button>-->
|
||||
</template>
|
||||
<!-- <template #description> 今日晴,20℃ - 32℃! </template>-->
|
||||
<template #description>
|
||||
{{ todayDate }}
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
@@ -256,10 +295,10 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<!-- <WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />-->
|
||||
<!-- <AnalysisChartCard class="mt-5" title="访问来源">-->
|
||||
<!-- <AnalyticsVisitsSource />-->
|
||||
<!-- </AnalysisChartCard>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, notification, Space } from 'ant-design-vue';
|
||||
|
||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
function info() {
|
||||
message.info('How many roads must a man walk down');
|
||||
}
|
||||
|
||||
function error() {
|
||||
message.error({
|
||||
content: 'Once upon a time you dressed so fine',
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
|
||||
function warning() {
|
||||
message.warning('How many roads must a man walk down');
|
||||
}
|
||||
function success() {
|
||||
message.success('Cause you walked hand in hand With another man in my place');
|
||||
}
|
||||
|
||||
function notify(type: NotificationType) {
|
||||
notification[type]({
|
||||
duration: 2500,
|
||||
message: '说点啥呢',
|
||||
type,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Ant Design Vue组件使用演示"
|
||||
>
|
||||
<Card class="mb-5" title="按钮">
|
||||
<Space>
|
||||
<Button>Default</Button>
|
||||
<Button type="primary"> Primary </Button>
|
||||
<Button> Info </Button>
|
||||
<Button danger> Error </Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card class="mb-5" title="Message">
|
||||
<Space>
|
||||
<Button @click="info"> 信息 </Button>
|
||||
<Button danger @click="error"> 错误 </Button>
|
||||
<Button @click="warning"> 警告 </Button>
|
||||
<Button @click="success"> 成功 </Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="Notification">
|
||||
<Space>
|
||||
<Button @click="notify('info')"> 信息 </Button>
|
||||
<Button danger @click="notify('error')"> 错误 </Button>
|
||||
<Button @click="notify('warning')"> 警告 </Button>
|
||||
<Button @click="notify('success')"> 成功 </Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
47
apps/web-antd/src/views/doctor/doctor-reception/api/index.ts
Normal file
47
apps/web-antd/src/views/doctor/doctor-reception/api/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
const prefix = 'doctor-reception/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getPatientList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}patient-list`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取患者详情
|
||||
* @param id
|
||||
*/
|
||||
export async function getPatientItem(id: number) {
|
||||
return requestClient.get<any>(`${prefix}patient-item`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
* @param id
|
||||
*/
|
||||
export async function getProductListDoctorReception(data: any) {
|
||||
return requestClient.get<any>(`${prefix}product-list`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取药品使用方式列表
|
||||
*/
|
||||
export async function getDrugUseList() {
|
||||
return requestClient.get<any>(`${prefix}drug-use-list`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 接诊
|
||||
*/
|
||||
export async function receptionApi(id) {
|
||||
return requestClient.post<any>(`${prefix}reception`, {id:id});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加西药药方
|
||||
* @param data
|
||||
*/
|
||||
export async function addWestPrescription(data: any) {
|
||||
return requestClient.post<any>(`${prefix}add-west-prescription`, data);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const item = ref<Record<string, any>>({});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) item.value = values;
|
||||
console.log(item.value, 'sssssssssssssssss');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function handleWindowPrint(ele, fileName) {
|
||||
// 获取要打印的元素
|
||||
const printBox = document.querySelector('.print-box');
|
||||
if (!printBox) {
|
||||
console.error('找不到具有 "print-box" 类的元素');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个隐藏的iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.right = '0';
|
||||
iframe.style.bottom = '0';
|
||||
iframe.style.width = '0';
|
||||
iframe.style.height = '0';
|
||||
iframe.style.border = '0';
|
||||
document.body.append(iframe);
|
||||
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
|
||||
// 写入HTML结构
|
||||
iframeDoc.open();
|
||||
iframeDoc.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${fileName || '文档打印'}</title>
|
||||
</head>
|
||||
<body>
|
||||
${printBox.outerHTML}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
// 复制原页面的所有样式
|
||||
const styles = document.querySelectorAll('style, link[rel="stylesheet"]');
|
||||
styles.forEach((style) => {
|
||||
if (style.tagName === 'LINK') {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = style.href; // 使用绝对路径
|
||||
iframeDoc.head.append(link);
|
||||
} else {
|
||||
iframeDoc.head.append(style.cloneNode(true));
|
||||
}
|
||||
});
|
||||
|
||||
iframeDoc.close();
|
||||
|
||||
// 加载完成后触发打印
|
||||
iframe.contentWindow.addEventListener('load', () => {
|
||||
iframe.contentWindow.print();
|
||||
// 打印后移除iframe
|
||||
setTimeout(() => {
|
||||
iframe.remove();
|
||||
}, 1000); // 确保打印对话框已弹出
|
||||
});
|
||||
}
|
||||
|
||||
// 解密方法示例(需要根据实际加密方式实现)
|
||||
const decrypt = (str: string) => str; // 简单base64解码示例
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[60%]" title="中药处方详情">
|
||||
<Page>
|
||||
<Button type="primary" @click="handleWindowPrint('#demo', '处方')">
|
||||
打印处方
|
||||
</Button>
|
||||
<div class="prescription-container print-box">
|
||||
<!-- 头部信息 -->
|
||||
<div class="prescription-header">
|
||||
<div class="header-top">
|
||||
<span>处方编号: {{ item.prescription_no }}</span>
|
||||
<div class="prescription-type">普通处方</div>
|
||||
</div>
|
||||
<h2 class="clinic-name">{{ item.content?.doctor.name }} 处方笺</h2>
|
||||
<div class="prescription-date">开具日期: {{ item.created_at }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 患者信息 -->
|
||||
<div class="patient-info">
|
||||
<div class="info-row">
|
||||
<span>姓名: {{ decrypt(item.content?.patient.name) }}</span>
|
||||
<span>性别: {{ item.content?.patient.sex === 1 ? '男' : '女' }}</span>
|
||||
<span>年龄: {{ item.content?.patient.age }}</span>
|
||||
<span>类别: {{ item.content?.category }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span>科室: {{ item.content?.doctor.depart?.name }}</span>
|
||||
<span>诊断: {{ item.content?.clinical_diagnose }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 药品列表 -->
|
||||
<div class="medicine-list">
|
||||
<div class="rp-title">Rp</div>
|
||||
<div
|
||||
v-for="(recipe, index) in item.content?.repice"
|
||||
:key="index"
|
||||
class="recipe-item"
|
||||
>
|
||||
<!-- <div class="medicine-item" v-for="drug in JSON.parse(recipe.content)" :key="drug?.id">-->
|
||||
<div class="medicine-item">
|
||||
<!-- {{JSON.parse(recipe.content)}}-->
|
||||
<span class="drug-name">{{
|
||||
JSON.parse(recipe.content).name ||
|
||||
JSON.parse(recipe.content).drug_name
|
||||
}}</span>
|
||||
<span class="drug-quantity">{{ JSON.parse(recipe.content).number
|
||||
}}{{ JSON.parse(recipe.content).unit?.name }}</span>
|
||||
<div v-if="JSON.parse(recipe.content).useWay" class="usage-info">
|
||||
{{ JSON.parse(recipe.content).useWay }}
|
||||
</div>
|
||||
<!-- <span class="drug-name">{{ drug?.name || drug?.drug_name }}</span>-->
|
||||
<!-- <span class="drug-quantity">{{ drug?.number }}{{ drug?.unit?.name }}</span>-->
|
||||
<!-- <div v-if="drug?.useWay" class="usage-info">-->
|
||||
<!-- {{ drug.useWay }}-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<!-- <div class="preparation-info">-->
|
||||
<!-- 煎服方法: 每日{{ recipe.deployment }}次,共{{ recipe.dosage }}剂-->
|
||||
<!-- </div>-->
|
||||
<div class="preparation-info">
|
||||
使用方法: {{ recipe.instruction }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 医嘱及签名 -->
|
||||
<div class="footer-section">
|
||||
<div class="medical-advice">
|
||||
<label>医嘱:</label>
|
||||
{{ item.content?.doctor_order }}
|
||||
</div>
|
||||
<div class="signature-area">
|
||||
<div class="signature">
|
||||
<label>开方医生:</label>
|
||||
{{ item.content?.doctor.name }}
|
||||
</div>
|
||||
<div class="signature">
|
||||
<label>审核药师:</label>
|
||||
{{ item?.first_view_info?.name || '' }}
|
||||
</div>
|
||||
<div class="signature">
|
||||
<label>调配人:</label>
|
||||
{{ item.content?.doctor.name }}
|
||||
</div>
|
||||
<div class="signature">
|
||||
<label>核对人:</label>
|
||||
{{ item?.again_view_info?.name || '' }}
|
||||
</div>
|
||||
<div class="signature">
|
||||
<label>发药人:</label>
|
||||
{{ item.content?.doctor.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-info">总价: ¥{{ item.total_pay_price }}</div>
|
||||
<div class="validity">处方有效期: {{ item.valid_hours }}小时</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.prescription-container {
|
||||
padding: 20px;
|
||||
font-family: 'SimSun', serif;
|
||||
}
|
||||
|
||||
.prescription-header {
|
||||
border-bottom: 2px solid #000;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.prescription-type {
|
||||
border: 1px solid #666;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.clinic-name {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.patient-info .info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.medicine-list {
|
||||
margin: 20px 0 200px 0;
|
||||
border-top: 1px solid #ccc;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.rp-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.recipe-item {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.medicine-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.drug-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.preparation-info {
|
||||
color: #666;
|
||||
margin-top: 12px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.medical-advice {
|
||||
color: #c00;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signature-area {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.signature {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
margin-top: 20px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.validity {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,549 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
ContainerOutlined,
|
||||
MinusOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardMeta,
|
||||
Col,
|
||||
Image,
|
||||
InputNumber,
|
||||
InputSearch,
|
||||
message,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getDrugUseList,
|
||||
getProductListDoctorReception,
|
||||
} from '#/views/doctor/doctor-reception/api';
|
||||
|
||||
const searchKey = ref('');
|
||||
const type = ref(1);
|
||||
const currentDrugs = ref();
|
||||
const activePatientId = ref(0);
|
||||
const drugList = ref([]);
|
||||
const selectList = ref([]);
|
||||
const drugUseNum = ref([]);
|
||||
const drugUseFrequency = ref([]);
|
||||
const drugUseType = ref([]);
|
||||
const drugUnit = ref([]);
|
||||
const drugTime = ref([]);
|
||||
const selectProductId = ref(0);
|
||||
const previewImage = ref('');
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const setVisible = (value, instruction = ''): void => {
|
||||
visible.value = value;
|
||||
previewImage.value = instruction;
|
||||
};
|
||||
|
||||
const getCurrentDrugs = () => {
|
||||
selectList.value = JSON.parse(
|
||||
localStorage.getItem(`prescriptionData${activePatientId.value}`) || '{}',
|
||||
);
|
||||
};
|
||||
/**
|
||||
* 获取商品列表
|
||||
* @param searchKey
|
||||
*/
|
||||
function getDrugListByWesternModal(searchKey = '') {
|
||||
if (searchKey === '' && type.value === 1) {
|
||||
drugList.value = [];
|
||||
return;
|
||||
}
|
||||
getCurrentDrugs();
|
||||
getProductListDoctorReception({
|
||||
store_id: 2,
|
||||
type: type.value,
|
||||
name: searchKey,
|
||||
}).then((res) => {
|
||||
// 循环res添加select_number = 0
|
||||
res.forEach((item) => {
|
||||
const matched = selectList.value.find(
|
||||
(value) => value.index_id === item.id,
|
||||
);
|
||||
item.select_number = matched ? matched.select_number : 0;
|
||||
item.drug.number = matched ? matched.number : 1;
|
||||
});
|
||||
drugList.value = res;
|
||||
});
|
||||
}
|
||||
function getDrugUseListByWesternModal() {
|
||||
getDrugUseList().then((res) => {
|
||||
drugUseNum.value = res.drug_use_num;
|
||||
drugUseFrequency.value = res.drug_use_frequency;
|
||||
drugUseType.value = res.drug_use_type;
|
||||
drugUnit.value = res.drug_unit;
|
||||
drugTime.value = res.drug_time;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 添加商品
|
||||
* @param {object} data - 商品对象
|
||||
*/
|
||||
function addProducts(data) {
|
||||
// 查找是否已存在
|
||||
const existItem = selectList.value.find((item) => item.index_id === data.id);
|
||||
|
||||
// 新建商品对象避免污染原始数据
|
||||
const newProduct = {
|
||||
index_id: data.id,
|
||||
id: data.drug.id,
|
||||
drug_name: data.drug.drug_name,
|
||||
number: data.drug.number,
|
||||
use_num: drugUseNum.value.find(
|
||||
(item) => item.id === data.drug.frequency_id,
|
||||
),
|
||||
use_type: drugUseType.value.find((item) => item.id === data.drug.type_id),
|
||||
use_frequency: drugUseFrequency.value.find(
|
||||
(item) => item.id === data.drug.frequency_id,
|
||||
),
|
||||
unit: drugUnit.value.find((item) => item.id === data.drug.unit_id),
|
||||
price: data.price,
|
||||
time_id: data.drug.time_id,
|
||||
type_id: data.drug.type_id,
|
||||
frequency_id: data.drug.frequency_id,
|
||||
unit_id: data.drug.unit_id,
|
||||
image: data.drug.image,
|
||||
instruction: data.drug.instruction,
|
||||
type: data.drug.type,
|
||||
};
|
||||
|
||||
if (existItem) {
|
||||
// 增量操作
|
||||
const newNumber = existItem.select_number + 1;
|
||||
|
||||
// 提前验证
|
||||
if (newNumber > 100) {
|
||||
message.error(`${existItem.drug_name}数量不能大于100!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新选中列表
|
||||
selectList.value = selectList.value.map((item) =>
|
||||
item.index_id === data.id ? { ...item, select_number: newNumber } : item,
|
||||
);
|
||||
|
||||
syncDrugList(data.id, newNumber); // 同步到药品列表
|
||||
message.success(`${existItem.drug_name}数量已增加至${newNumber}!`);
|
||||
} else {
|
||||
// 首次添加,初始化数量为1
|
||||
newProduct.select_number = 1;
|
||||
|
||||
// 添加前验证
|
||||
if (newProduct.select_number > 100) {
|
||||
message.error(`${newProduct.drug_name}数量不能大于100!`);
|
||||
return;
|
||||
}
|
||||
|
||||
selectList.value.push(newProduct);
|
||||
console.log(selectList.value, 'ssssssssss');
|
||||
syncDrugList(newProduct.index_id, 1); // 同步到药品列表
|
||||
message.success(`已将${newProduct.drug_name}添加到清单中!`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少商品
|
||||
* @param {object} data - 商品对象
|
||||
*/
|
||||
function propProducts(data) {
|
||||
const existItem = selectList.value.find((item) => item.index_id === data.id);
|
||||
if (!existItem) {
|
||||
message.error(`${data.drug.drug_name}不在清单中!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newNumber = existItem.select_number - 1;
|
||||
|
||||
// 提前验证
|
||||
if (newNumber < 0) {
|
||||
message.error(`${data.drug.drug_name}数量不能小于0!`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newNumber === 0) {
|
||||
// 移出清单
|
||||
selectList.value = selectList.value.filter(
|
||||
(item) => item.index_id !== data.id,
|
||||
);
|
||||
syncDrugList(data.id, 0); // 同步清零
|
||||
message.success(`${data.drug.drug_name}已从清单移出!`);
|
||||
} else {
|
||||
// 更新数量
|
||||
selectList.value = selectList.value.map((item) =>
|
||||
item.index_id === data.id ? { ...item, select_number: newNumber } : item,
|
||||
);
|
||||
syncDrugList(data.id, newNumber); // 同步更新
|
||||
message.success(`${data.drug.drug_name}数量已减少至${newNumber}!`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步更新药品列表
|
||||
* @param {number} productId - 药品ID
|
||||
* @param {number} number - 新的数量
|
||||
*/
|
||||
function syncDrugList(productId, number) {
|
||||
drugList.value = drugList.value.map((item) =>
|
||||
item.id === productId ? { ...item, select_number: number } : item,
|
||||
);
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
localStorage.setItem(
|
||||
`prescriptionData${activePatientId.value}`,
|
||||
// JSON.stringify(selectData),
|
||||
JSON.stringify(selectList.value),
|
||||
);
|
||||
currentDrugs.value();
|
||||
modalApi.close();
|
||||
message.success('保存成功');
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
// getCurrentDrugs
|
||||
currentDrugs.value = isOpen ? modalApi.getData()?.getCurrentDrugs : null;
|
||||
const { values, activePatient_id } =
|
||||
modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
type.value = values;
|
||||
activePatientId.value = activePatient_id;
|
||||
getDrugListByWesternModal();
|
||||
getDrugUseListByWesternModal();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function selectProductChange(id) {
|
||||
selectProductId.value = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择频次
|
||||
* @param id
|
||||
*/
|
||||
function selectFrequencyChange(id) {
|
||||
drugList.value = drugList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
drug: {
|
||||
...item.drug,
|
||||
frequency_id: id,
|
||||
},
|
||||
}
|
||||
: item,
|
||||
);
|
||||
selectList.value = selectList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.index_id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
frequency_id: id,
|
||||
use_frequency: drugUseFrequency.value.find(
|
||||
(value) => value.id === id,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择使用时间
|
||||
* @param id
|
||||
*/
|
||||
function selectTimeChange(id) {
|
||||
drugList.value = drugList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
drug: {
|
||||
...item.drug,
|
||||
time_id: id,
|
||||
},
|
||||
}
|
||||
: item,
|
||||
);
|
||||
selectList.value = selectList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.index_id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
time_id: id,
|
||||
use_num: drugTime.value.find((value) => value.id === id),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择使用方法
|
||||
* @param id
|
||||
*/
|
||||
function selectTypeChange(id) {
|
||||
drugList.value = drugList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
drug: {
|
||||
...item.drug,
|
||||
type_id: id,
|
||||
},
|
||||
}
|
||||
: item,
|
||||
);
|
||||
selectList.value = selectList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.index_id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
type_id: id,
|
||||
use_type: drugUseType.value.find((value) => value.id === id),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择单位
|
||||
* @param id
|
||||
*/
|
||||
function selectUnitChange(id) {
|
||||
drugList.value = drugList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
drug: {
|
||||
...item.drug,
|
||||
unit_id: id,
|
||||
},
|
||||
}
|
||||
: item,
|
||||
);
|
||||
selectList.value = selectList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.index_id === selectProductId.value
|
||||
? {
|
||||
...item,
|
||||
unit_id: id,
|
||||
unit: drugUnit.value.find((value) => value.id === id),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function updateProductNumber(id, number) {
|
||||
if (number > 100 || number <= 0) {
|
||||
message.error('数量不能大于100或小于1');
|
||||
return;
|
||||
}
|
||||
drugList.value = drugList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.id === id
|
||||
? {
|
||||
...item,
|
||||
drug: {
|
||||
...item.drug,
|
||||
number,
|
||||
},
|
||||
}
|
||||
: item,
|
||||
);
|
||||
|
||||
selectList.value = selectList.value.map((item) =>
|
||||
// 修改item下面drug的 frequency_id = id
|
||||
item.index_id === id
|
||||
? {
|
||||
...item,
|
||||
number,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <Modal class="w-[60%]" title="西(中成)药列表">-->
|
||||
<Modal class="w-[60%]" title="商品列表">
|
||||
<Image
|
||||
:preview="{
|
||||
visible,
|
||||
onVisibleChange: setVisible,
|
||||
}"
|
||||
:src="previewImage"
|
||||
:style="{ display: 'none' }"
|
||||
:width="200"
|
||||
/>
|
||||
<Page>
|
||||
<Row :gutter="12">
|
||||
<Col :span="24">
|
||||
<InputSearch
|
||||
:model="searchKey"
|
||||
enter-button
|
||||
placeholder="请输入商品名称或拼音首拼"
|
||||
@search="getDrugListByWesternModal"
|
||||
/>
|
||||
</Col>
|
||||
<Col v-for="item in drugList" :key="item.id" :span="6">
|
||||
<Badge :count="item.select_number" class="mt-5">
|
||||
<Card hoverable>
|
||||
<template #cover>
|
||||
<Image :height="200" :src="item.drug?.image" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<MinusOutlined key="prop" @click="propProducts(item)" />
|
||||
<ContainerOutlined
|
||||
@click="setVisible(true, item.drug.instruction)"
|
||||
/>
|
||||
<PlusOutlined key="add" @click="addProducts(item)" />
|
||||
<!-- <EllipsisOutlined key="ellipsis" />-->
|
||||
</template>
|
||||
<CardMeta
|
||||
:description="item.drug?.pinyin_simple"
|
||||
:title="item.drug?.drug_name"
|
||||
>
|
||||
<template #avatar>
|
||||
<a-avatar src="https://joeschmoe.io/api/v1/random" />
|
||||
</template>
|
||||
</CardMeta>
|
||||
<div class="card-box mt-5" style="padding: 10px 5px">
|
||||
<Popover>
|
||||
<p class="drug-function text-xs">
|
||||
功效:{{ item.drug?.function }}
|
||||
</p>
|
||||
|
||||
<template #content>
|
||||
<p>功效:{{ item.drug?.function }}</p>
|
||||
<p>用法:{{ item.drug?.usage }}</p>
|
||||
</template>
|
||||
</Popover>
|
||||
<p class="text-source mt-5">{{ item.drug?.source }}</p>
|
||||
<!-- 选择频次-->
|
||||
<Select
|
||||
:value="item.drug?.frequency_id"
|
||||
class="mt-3 w-1/3"
|
||||
@change="selectFrequencyChange"
|
||||
@dropdown-visible-change="selectProductChange(item.id)"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(value, index) in drugUseFrequency"
|
||||
:key="index"
|
||||
:value="value.id"
|
||||
>
|
||||
{{ value.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
<!-- 选择时间-->
|
||||
<Select
|
||||
:value="item.drug?.time_id"
|
||||
class="mt-3 w-1/3"
|
||||
@change="selectTimeChange"
|
||||
@dropdown-visible-change="selectProductChange(item.id)"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(value, index) in drugTime"
|
||||
:key="index"
|
||||
:value="value.id"
|
||||
>
|
||||
{{ value.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
<!-- 选择使用方法-->
|
||||
<Select
|
||||
:value="item.drug?.type_id"
|
||||
class="mt-3 w-1/3"
|
||||
@change="selectTypeChange"
|
||||
@dropdown-visible-change="selectProductChange(item.id)"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(value, index) in drugUseType"
|
||||
:key="index"
|
||||
:value="value.id"
|
||||
>
|
||||
{{ value.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
<div class="mt-3">
|
||||
<!-- 选择单位-->
|
||||
<InputNumber :value="item.drug.number" class="w-1/2">
|
||||
<template #addonBefore>
|
||||
<MinusOutlined
|
||||
key="prop"
|
||||
@click="
|
||||
updateProductNumber(
|
||||
item.id,
|
||||
item.drug.number > 1 ? item.drug.number - 1 : 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #addonAfter>
|
||||
<PlusOutlined
|
||||
key="add"
|
||||
@click="
|
||||
updateProductNumber(
|
||||
item.id,
|
||||
item.drug.number < 100 ? item.drug.number + 1 : 100,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</InputNumber>
|
||||
<Select
|
||||
:value="item.drug?.unit_id"
|
||||
class="w-1/2"
|
||||
@change="selectUnitChange"
|
||||
@dropdown-visible-change="selectProductChange(item.id)"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="(value, index) in drugUnit"
|
||||
:key="index"
|
||||
:value="value.id"
|
||||
>
|
||||
{{ value.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Badge>
|
||||
</Col>
|
||||
</Row>
|
||||
</Page>
|
||||
</Modal>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.drug-function {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.text-source {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
840
apps/web-antd/src/views/doctor/doctor-reception/index.vue
Normal file
840
apps/web-antd/src/views/doctor/doctor-reception/index.vue
Normal file
@@ -0,0 +1,840 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch} from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Descriptions,
|
||||
Empty,
|
||||
Image,
|
||||
ImagePreviewGroup,
|
||||
InputNumber,
|
||||
message,
|
||||
Tag,
|
||||
Textarea,
|
||||
Timeline,
|
||||
TimelineItem,
|
||||
RadioGroup,
|
||||
RadioButton,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getRegisterStatus } from '#/util/tool';
|
||||
import {
|
||||
addWestPrescription,
|
||||
getPatientItem,
|
||||
getPatientList,
|
||||
receptionApi,
|
||||
} from '#/views/doctor/doctor-reception/api';
|
||||
|
||||
import PrescrtionDetail from './components/PrescrtionDetail.vue';
|
||||
import WesternModal from './components/WesternModal.vue';
|
||||
|
||||
interface Patient {
|
||||
id: number;
|
||||
name: string;
|
||||
mobile: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface UserPatientHealthInquiry {
|
||||
id: number;
|
||||
user_patient_id: number;
|
||||
liver_function: number;
|
||||
renal_function: number;
|
||||
liver_index: string;
|
||||
renal_index: string;
|
||||
person_history: string;
|
||||
allergic_history: string;
|
||||
person_status: number;
|
||||
allergic_status: number;
|
||||
family_status: number;
|
||||
family_history: number;
|
||||
is_delete: number;
|
||||
}
|
||||
|
||||
const tabType = ref(0);
|
||||
const category = ref(0);
|
||||
const receptionStatus = ref(0);
|
||||
const updateTabType = ref(false);
|
||||
// 患者数据
|
||||
const patients = ref<Patient[]>([]);
|
||||
|
||||
/**
|
||||
* 获取患者列表
|
||||
*/
|
||||
function getPatientListByReception() {
|
||||
getPatientList({}).then((res) => {
|
||||
patients.value = res;
|
||||
|
||||
if (updateTabType.value === false) {
|
||||
const activePatientId = localStorage.getItem(`doctorReception-id`);
|
||||
if (activePatientId !== null) {
|
||||
// 从patients.value中找到当前选中的id并且触发selectPatient方法
|
||||
patients.value.forEach((patient) => {
|
||||
if (patient.id === Number.parseInt(activePatientId)) {
|
||||
selectPatient(patient, false);
|
||||
updateTabType.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
getPatientListByReception();
|
||||
// 每5秒更新数据
|
||||
setInterval(getPatientListByReception, 30_000);
|
||||
|
||||
// 药品分类
|
||||
const categories = [
|
||||
{ label: '中药', value: 1 },
|
||||
{ label: '中成(西)药', value: 2 },
|
||||
// { label: '西药', value: 2 },
|
||||
{ label: '保健商品', value: 3 },
|
||||
// { label: '中成药商品', value: 4 },
|
||||
{ label: '服务包', value: 5 },
|
||||
];
|
||||
|
||||
// 当前状态
|
||||
const activePatient = ref<null | Patient>(null);
|
||||
const patientInfo = ref<null | Patient>(null);
|
||||
const userPatientHealthInquiry = ref<null | UserPatientHealthInquiry>(null);
|
||||
const activeCategory = ref(2);
|
||||
const diagnosis = ref('');
|
||||
const medicalAdvice = ref('');
|
||||
const treatmentPrice = ref(0);
|
||||
const selectPatientId = ref(0);
|
||||
const prescriptionList = ref([]);
|
||||
|
||||
const totalCost = computed(() => {
|
||||
if (currentDrugs.value.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return currentDrugs.value.reduce(
|
||||
(sum, drug) => sum + drug.price * (drug.select_number || 1),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
const visible = ref<boolean>(false);
|
||||
const previewImage = ref([]);
|
||||
/**
|
||||
* 说明书预览
|
||||
* @param value
|
||||
* @param instruction
|
||||
*/
|
||||
const setVisible = (value, instruction = ''): void => {
|
||||
previewImage.value = [];
|
||||
if (instruction == '') {
|
||||
message.error('该产品没有说明书!');
|
||||
} else {
|
||||
if (typeof instruction === 'string') {
|
||||
// 如果有逗号,则进行分割 没有则追加到previewImage
|
||||
console.log(instruction, value, 'sssssss');
|
||||
if (instruction.includes(',')) {
|
||||
instruction = instruction.split(',');
|
||||
previewImage.value = instruction;
|
||||
} else {
|
||||
previewImage.value.push(instruction);
|
||||
}
|
||||
}
|
||||
visible.value = value;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 选择华女这操作
|
||||
* @param patient
|
||||
* @param isUpdateTabType
|
||||
*/
|
||||
const selectPatient = (patient: Patient, isUpdateTabType = true) => {
|
||||
selectPatientId.value = patient.id;
|
||||
receptionStatus.value = patient.status;
|
||||
if (isUpdateTabType === true) {
|
||||
tabType.value = 1;
|
||||
} else {
|
||||
// 获取doctorReception-type并赋值
|
||||
if (localStorage.getItem(`doctorReception-type`) !== null) {
|
||||
tabType.value = Number.parseInt(
|
||||
localStorage.getItem(`doctorReception-type`),
|
||||
);
|
||||
}
|
||||
}
|
||||
activePatient.value = patient.user_patient;
|
||||
getPatientItem(patient.id).then((value) => {
|
||||
patientInfo.value = value;
|
||||
userPatientHealthInquiry.value = value.user_patient_health_inquiry;
|
||||
prescriptionList.value = value.prescription;
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
`doctorReception-id`,
|
||||
// JSON.stringify(selectData),
|
||||
JSON.stringify(patient.id),
|
||||
);
|
||||
getCurrentDrugs();
|
||||
};
|
||||
|
||||
// 数量增加
|
||||
const increment = (index: number) => {
|
||||
currentDrugs.value[index].select_number++;
|
||||
saveToLocalStorage();
|
||||
};
|
||||
|
||||
// 数量减少
|
||||
const decrement = (index: number) => {
|
||||
if (currentDrugs.value[index].select_number > 1) {
|
||||
currentDrugs.value[index].select_number--;
|
||||
saveToLocalStorage();
|
||||
}
|
||||
};
|
||||
|
||||
// 保存到本地存储
|
||||
const saveToLocalStorage = () => {
|
||||
localStorage.setItem(
|
||||
`prescriptionData${activePatient.value?.id}`,
|
||||
JSON.stringify(currentDrugs.value),
|
||||
);
|
||||
};
|
||||
/**
|
||||
* 删除药品(本地)
|
||||
* @param index
|
||||
*/
|
||||
const removeDrug = (index: number) => {
|
||||
currentDrugs.value.splice(index, 1);
|
||||
updateLocalStorage();
|
||||
};
|
||||
/**
|
||||
* 修改缓存的处方信息
|
||||
*/
|
||||
const updateLocalStorage = () => {
|
||||
localStorage.setItem(
|
||||
`prescriptionData${activePatient.value?.id}`,
|
||||
JSON.stringify(currentDrugs.value),
|
||||
);
|
||||
getCurrentDrugs();
|
||||
};
|
||||
|
||||
const currentDrugs = ref([]);
|
||||
/**
|
||||
* 获取缓存的处方信息
|
||||
*/
|
||||
const getCurrentDrugs = () => {
|
||||
currentDrugs.value = JSON.parse(
|
||||
localStorage.getItem(`prescriptionData${activePatient.value?.id}`) || '[]',
|
||||
);
|
||||
};
|
||||
getCurrentDrugs();
|
||||
/**
|
||||
* 发送处方
|
||||
*/
|
||||
const sendPrescription = () => {
|
||||
if (diagnosis.value === '') {
|
||||
message.error('诊断结果不能为空');
|
||||
return;
|
||||
}
|
||||
if (medicalAdvice.value === '') {
|
||||
message.error('医嘱不能为空');
|
||||
return;
|
||||
}
|
||||
// 发送处方逻辑
|
||||
console.log('处方已发送', {
|
||||
patient: activePatient.value,
|
||||
drugs: currentDrugs.value,
|
||||
diagnosis: diagnosis.value,
|
||||
medicalAdvice: medicalAdvice.value,
|
||||
total: totalCost.value,
|
||||
});
|
||||
addWestPrescription({
|
||||
patient: activePatient.value,
|
||||
drugs: currentDrugs.value,
|
||||
diagnosis: diagnosis.value,
|
||||
medicalAdvice: medicalAdvice.value,
|
||||
total: totalCost.value,
|
||||
category: category.value,
|
||||
drug_type: tabType.value,
|
||||
register_id: Number.parseInt(localStorage.getItem(`doctorReception-id`)),
|
||||
treatment_price: treatmentPrice.value,
|
||||
}).then(() => {
|
||||
message.success('处方已发送');
|
||||
// 清空当前数据
|
||||
// currentDrugs.value = [];
|
||||
// diagnosis.value = '';
|
||||
// medicalAdvice.value = '';
|
||||
// updateLocalStorage();
|
||||
});
|
||||
};
|
||||
|
||||
// 进入处方界面
|
||||
const showPrescription = () => {
|
||||
// 打开处方模态框逻辑
|
||||
tabType.value = 2;
|
||||
};
|
||||
|
||||
// 监听tabType.value变化
|
||||
watch(
|
||||
() => tabType.value,
|
||||
(newValue) => {
|
||||
if (newValue === 1) {
|
||||
getPatientItem(selectPatientId.value).then((value) => {
|
||||
patientInfo.value = value;
|
||||
userPatientHealthInquiry.value = value.user_patient_health_inquiry;
|
||||
prescriptionList.value = value.prescription;
|
||||
});
|
||||
}
|
||||
localStorage.setItem(
|
||||
`doctorReception-type`,
|
||||
// JSON.stringify(selectData),
|
||||
JSON.stringify(newValue),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const [WesternDrugModal, WesternDrugModalApi] = useVbenModal({
|
||||
connectedComponent: WesternModal,
|
||||
});
|
||||
const [PrescrtionDetailModal, PrescrtionDetailModalApi] = useVbenModal({
|
||||
connectedComponent: PrescrtionDetail,
|
||||
});
|
||||
const openWesternModal = () => {
|
||||
// 打开西药处方模态框逻辑
|
||||
WesternDrugModalApi.setData({
|
||||
values: activeCategory.value,
|
||||
activePatient_id: activePatient.value?.id,
|
||||
getCurrentDrugs,
|
||||
});
|
||||
WesternDrugModalApi.open();
|
||||
};
|
||||
const openPrescriptionDetail = (values) => {
|
||||
// 打开西药处方模态框逻辑
|
||||
PrescrtionDetailModalApi.setData({
|
||||
values,
|
||||
});
|
||||
PrescrtionDetailModalApi.open();
|
||||
};
|
||||
|
||||
// 根据创制string来把字符串的逗号分开变成数组
|
||||
function splitString(str: string) {
|
||||
if (!str) {
|
||||
return [];
|
||||
}
|
||||
return str.split(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换tab
|
||||
* @param id
|
||||
*/
|
||||
function tabChange(id) {
|
||||
localStorage.setItem(
|
||||
`prescriptionData${activePatient.value?.id}`,
|
||||
JSON.stringify([]),
|
||||
);
|
||||
currentDrugs.value = [];
|
||||
activeCategory.value = id;
|
||||
}
|
||||
|
||||
function reception() {
|
||||
receptionApi(localStorage.getItem(`doctorReception-id`)).then((value) => {
|
||||
message.success('接诊成功');
|
||||
updateTabType.value = false;
|
||||
getPatientListByReception();
|
||||
receptionStatus.value = 2;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<!-- 左侧患者列表 -->
|
||||
<div class="patient-panel">
|
||||
<div
|
||||
v-for="patient in patients"
|
||||
:key="patient.user_patient.id"
|
||||
:class="{ active: selectPatientId === patient.id }"
|
||||
class="patient-card"
|
||||
@click="selectPatient(patient)"
|
||||
>
|
||||
<div class="patient-info">
|
||||
<h3>{{ patient.user_patient.name }}</h3>
|
||||
<p class="phone">{{ patient.user_patient.mobile }}</p>
|
||||
<span :class="getRegisterStatus(patient.status)" class="status">{{
|
||||
getRegisterStatus(patient.status)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧处方区域 -->
|
||||
<Page v-if="tabType === 1" class="prescription-panel">
|
||||
<div class="prescription-header">
|
||||
<Button v-if="receptionStatus === 1" class="send-btn" type="primary" @click="reception">
|
||||
接诊
|
||||
</Button>
|
||||
<Button v-if="receptionStatus === 2" class="send-btn" type="primary" @click="showPrescription">
|
||||
开具处方
|
||||
</Button>
|
||||
</div>
|
||||
<div class="prescription-content">
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
|
||||
bordered
|
||||
title="患者信息"
|
||||
>
|
||||
<Descriptions.Item label="患者姓名">
|
||||
{{ activePatient.name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="患者年龄">
|
||||
{{ activePatient.age }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="患者性别">
|
||||
{{
|
||||
activePatient.sex === 1
|
||||
? '男'
|
||||
: activePatient.sex === 2
|
||||
? '女'
|
||||
: '未填写'
|
||||
}}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Descriptions
|
||||
v-if="userPatientHealthInquiry"
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
|
||||
bordered
|
||||
class="mt-5"
|
||||
title="健康信息"
|
||||
>
|
||||
<Descriptions.Item label="肝功能">
|
||||
<span>{{
|
||||
userPatientHealthInquiry.liver_function === 0 ? '正常' : '异常'
|
||||
}}</span>
|
||||
<p
|
||||
v-if="userPatientHealthInquiry.liver_function === 1"
|
||||
class="mt-3"
|
||||
>
|
||||
<Tag
|
||||
v-for="item in splitString(
|
||||
userPatientHealthInquiry.liver_index,
|
||||
)"
|
||||
color="#455cda"
|
||||
>
|
||||
{{ item }}
|
||||
</Tag>
|
||||
</p>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="肾功能">
|
||||
<span>{{
|
||||
userPatientHealthInquiry.renal_function === 0 ? '正常' : '异常'
|
||||
}}</span>
|
||||
<p
|
||||
v-if="userPatientHealthInquiry.renal_function === 1"
|
||||
class="mt-3"
|
||||
>
|
||||
<Tag
|
||||
v-for="item in splitString(
|
||||
userPatientHealthInquiry.renal_index,
|
||||
)"
|
||||
color="#455cda"
|
||||
>
|
||||
{{ item }}
|
||||
</Tag>
|
||||
</p>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="既往史">
|
||||
<span>{{
|
||||
userPatientHealthInquiry.person_status === 0 ? '无' : '有'
|
||||
}}</span>
|
||||
<p v-if="userPatientHealthInquiry.person_status === 1" class="mt-3">
|
||||
<Tag
|
||||
v-for="item in splitString(
|
||||
userPatientHealthInquiry.person_history,
|
||||
)"
|
||||
color="#455cda"
|
||||
>
|
||||
{{ item }}
|
||||
</Tag>
|
||||
</p>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="过敏史">
|
||||
<span>{{
|
||||
userPatientHealthInquiry.allergic_status === 0 ? '无' : '有'
|
||||
}}</span>
|
||||
<p
|
||||
v-if="userPatientHealthInquiry.allergic_status === 1"
|
||||
class="mt-3"
|
||||
>
|
||||
<Tag
|
||||
v-for="item in splitString(
|
||||
userPatientHealthInquiry.allergic_history,
|
||||
)"
|
||||
color="#455cda"
|
||||
>
|
||||
{{ item }}
|
||||
</Tag>
|
||||
</p>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="家庭遗传史">
|
||||
<span>{{
|
||||
userPatientHealthInquiry.family_status === 0 ? '无' : '有'
|
||||
}}</span>
|
||||
<p v-if="userPatientHealthInquiry.family_status === 1" class="mt-3">
|
||||
<Tag
|
||||
v-for="item in splitString(
|
||||
userPatientHealthInquiry.family_history,
|
||||
)"
|
||||
color="#455cda"
|
||||
>
|
||||
{{ item }}
|
||||
</Tag>
|
||||
</p>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Descriptions
|
||||
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
|
||||
class="mt-5"
|
||||
title="处方记录"
|
||||
>
|
||||
<Descriptions.Item>
|
||||
<Timeline>
|
||||
<TimelineItem v-for="item in prescriptionList" :key="item.id">
|
||||
<Tag color="#455cda">{{ item.prescription_no }}</Tag>
|
||||
<Button type="link" @click="openPrescriptionDetail(item)">
|
||||
查看处方
|
||||
</Button>
|
||||
<span class="time-line-item-created">{{
|
||||
item.created_at
|
||||
}}</span>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
<Page v-if="tabType === 2" class="prescription-panel">
|
||||
<Button class="mb-10" type="primary" @click="tabType = 1">
|
||||
返回上一页
|
||||
</Button>
|
||||
<div class="prescription-header">
|
||||
<h2>{{ activePatient.name }} 的处方</h2>
|
||||
<Button class="send-btn" type="primary" @click="sendPrescription">
|
||||
发送处方
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 药品分类导航 -->
|
||||
<div class="drug-categories">
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="category.value"
|
||||
:class="{ active: activeCategory === category.value }"
|
||||
@click="tabChange(category.value)"
|
||||
>
|
||||
{{ category.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<Button type="primary" @click="openWesternModal"> 添加药品 </Button>
|
||||
<RadioGroup v-model:value="category">
|
||||
<RadioButton value="1">自费</RadioButton>
|
||||
<RadioButton value="2">医保</RadioButton>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div style="width: 0; height: 0; overflow: hidden">
|
||||
<ImagePreviewGroup
|
||||
:preview="{
|
||||
visible,
|
||||
onVisibleChange: setVisible,
|
||||
}"
|
||||
:style="{ display: 'none' }"
|
||||
>
|
||||
<Image
|
||||
v-for="image in previewImage"
|
||||
v-if="visible === true"
|
||||
:src="image"
|
||||
:width="200"
|
||||
/>
|
||||
</ImagePreviewGroup>
|
||||
</div>
|
||||
<!-- 已选药品列表 -->
|
||||
<div class="selected-drugs">
|
||||
<div class="drug-table">
|
||||
<div class="table-header">
|
||||
<span>商品图片</span>
|
||||
<span>商品名称</span>
|
||||
<span>数量</span>
|
||||
<span>单价</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<div
|
||||
v-for="(drug, index) in currentDrugs"
|
||||
:key="drug.id"
|
||||
class="table-row"
|
||||
>
|
||||
<div style="width: 120px; margin: 0 auto;">
|
||||
<Image :src="drug.image" />
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<span>药品名称:{{ drug.drug_name }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span
|
||||
>用法:{{
|
||||
`${drug.use_type?.name},${drug.use_num?.name},${drug.use_type?.name},每次${drug.number}${drug.unit?.name}`
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="quantity-control">
|
||||
<Button type="primary" @click="decrement(index)">-</Button>
|
||||
<span style="margin: 0 20px">{{ drug.select_number }}</span>
|
||||
<Button type="primary" @click="increment(index)">+</Button>
|
||||
</div>
|
||||
<span>{{ drug.price }}</span>
|
||||
<div>
|
||||
<Button type="link" @click="removeDrug(index)">删除</Button>
|
||||
<Button
|
||||
v-if="drug.instruction !== ''"
|
||||
type="link"
|
||||
@click="setVisible(true, drug.instruction)"
|
||||
>
|
||||
查看说明书
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 诊断和医嘱 -->
|
||||
<div class="diagnosis-area">
|
||||
<Textarea
|
||||
v-model:value="diagnosis"
|
||||
placeholder="输入诊断结果..."
|
||||
/>
|
||||
<Textarea
|
||||
v-model:value="medicalAdvice"
|
||||
placeholder="输入医嘱..."
|
||||
/>
|
||||
<InputNumber
|
||||
v-model:value="treatmentPrice"
|
||||
placeholder="请输入诊疗价格"
|
||||
>
|
||||
<template #addonAfter> /元 </template>
|
||||
</InputNumber>
|
||||
</div>
|
||||
|
||||
<!-- 费用总计 -->
|
||||
<div class="total-cost">总计:¥{{ totalCost.toFixed(2) }}</div>
|
||||
</Page>
|
||||
<div v-else-if="tabType === 0" class="prescription-panel">
|
||||
<Empty />
|
||||
</div>
|
||||
<WesternDrugModal />
|
||||
<PrescrtionDetailModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
min-height: 90vh;
|
||||
}
|
||||
|
||||
.patient-panel {
|
||||
border-right: 1px solid #e4e7ed;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dark .patient-panel {
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
.patient-card {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.dark .patient-card {
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.patient-card.active {
|
||||
border-left: 4px solid #455cda;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.status.待支付 {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
.status.待接诊 {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.status.接诊中 {
|
||||
background: #f0f9eb;
|
||||
color: #67c23a;
|
||||
}
|
||||
.status.已结束 {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
.status.已取消 {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.status.待评价 {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
.status.已评价 {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
.status.已拒诊 {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.dark .status.待接诊,
|
||||
.dark .status.已接诊 {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.prescription-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.drug-categories {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.drug-categories button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark .drug-categories button {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.drug-categories button.active {
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
animation: pulse 0.5s /*infinite*/;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
background: #455cda;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.drug-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.drug-item:hover {
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.selected-drugs {
|
||||
margin: 1rem 0;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dark .selected-drugs {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
max-height: 30vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr;
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 1fr 1fr 1fr 1fr;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
text-align: center;
|
||||
height: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dark .table-row {
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.diagnosis-area textarea {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 8px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark .diagnosis-area textarea {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.total-cost {
|
||||
text-align: right;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
color: #f56c6c;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.prescription-content {
|
||||
margin-top: 3%;
|
||||
}
|
||||
|
||||
.time-line-item-created {
|
||||
color: #909399;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
61
apps/web-antd/src/views/finance/withdrawal/api/index.ts
Normal file
61
apps/web-antd/src/views/finance/withdrawal/api/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
const prefix = 'withdrawal-application/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getWithdrawalApplicationList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提现记录详情
|
||||
* @param id
|
||||
*/
|
||||
export async function getWithdrawalApplicationInfo(id: number) {
|
||||
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增提现记录
|
||||
* @param data
|
||||
*/
|
||||
export async function createWithdrawalApplicationWithdrawal(
|
||||
data: Record<string, any>,
|
||||
) {
|
||||
return requestClient.post<any>(`${prefix}withdrawal`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增提现记录
|
||||
* @param data
|
||||
*/
|
||||
export async function createWithdrawalApplication(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑提现记录
|
||||
* @param data
|
||||
*/
|
||||
export async function updateWithdrawalApplication(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除提现记录
|
||||
* @param data
|
||||
*/
|
||||
export async function deleteWithdrawalApplication(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除提现记录
|
||||
* @param id
|
||||
*/
|
||||
export async function resetPassword(id: number) {
|
||||
return requestClient.post<any>(`${prefix}reset-password`, {
|
||||
id,
|
||||
});
|
||||
}
|
||||
51
apps/web-antd/src/views/finance/withdrawal/api/settlement.ts
Normal file
51
apps/web-antd/src/views/finance/withdrawal/api/settlement.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
const prefix = 'settlement/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getSettlementList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理员详情
|
||||
* @param id
|
||||
*/
|
||||
export async function getSettlementInfo(id: number) {
|
||||
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增管理员
|
||||
* @param data
|
||||
*/
|
||||
export async function createSettlement(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑管理员
|
||||
* @param data
|
||||
*/
|
||||
export async function updateSettlement(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除管理员
|
||||
* @param data
|
||||
*/
|
||||
export async function deleteSettlement(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除管理员
|
||||
* @param id
|
||||
*/
|
||||
export async function resetPassword(id: number) {
|
||||
return requestClient.post<any>(`${prefix}reset-password`, {
|
||||
id,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { saveCard } from '#/views/system/admin/api';
|
||||
|
||||
import { modalCardFormProps } from '../config/form';
|
||||
|
||||
const gridApi = ref();
|
||||
const showCard = ref();
|
||||
const maxAmount = ref(0);
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalCardFormProps);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = saveCard;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
// gridApi.value?.reload();
|
||||
showCard.value();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
showCard.value = isOpen ? modalApi.getData()?.showCard : null;
|
||||
if (isOpen) {
|
||||
const { values, balance } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
maxAmount.value = balance;
|
||||
formApi.setValues(values);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[30%]" title="编辑银行卡账户">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { type AnalysisOverviewItem, useVbenModal } from '@vben/common-ui';
|
||||
import { AnalysisOverview } from '@vben/common-ui';
|
||||
import { SvgCakeIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Card, message } from 'ant-design-vue';
|
||||
|
||||
import { getAdminAccountBalance, getMyCard } from '#/views/system/admin/api';
|
||||
|
||||
import SaveMyCard from '../components/card-modal.vue';
|
||||
import FormModalDemo from '../components/withdrawal-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'Statistics',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
gridApi: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const balance = ref(0);
|
||||
const pendingEarnings = ref(0);
|
||||
const settledEarnings = ref(0);
|
||||
const totalEarnings = ref(0);
|
||||
const withdrawnAmount = ref(0);
|
||||
const withdrawnFrozenAmount = ref(0);
|
||||
const frozenAmount = ref(0);
|
||||
// const arrivedAmount = ref(0);
|
||||
const fee = ref(0);
|
||||
const myCard = ref();
|
||||
|
||||
const overviewItems = ref<AnalysisOverviewItem[]>([]);
|
||||
|
||||
/**
|
||||
* 获取余额卡片列表数据
|
||||
*/
|
||||
function showCard() {
|
||||
showMyCard();
|
||||
getAdminAccountBalance().then((res) => {
|
||||
balance.value = res.balance;
|
||||
pendingEarnings.value = res.pending_earnings; // 待结算
|
||||
settledEarnings.value = res.settled_earnings;
|
||||
totalEarnings.value = res.total; // 累计收益
|
||||
withdrawnAmount.value = res.withdrawn; // 已提现金额
|
||||
withdrawnFrozenAmount.value = res.withdrawn_frozen; // 已提现金额
|
||||
frozenAmount.value = res.frozen; // 已提现金额
|
||||
// arrivedAmount.value = res.data.arrived_amount.toNumber();
|
||||
fee.value = res.charge;
|
||||
overviewItems.value = [
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '累计收益',
|
||||
totalTitle: '累计收益',
|
||||
totalValue: totalEarnings.value,
|
||||
value: totalEarnings.value,
|
||||
},
|
||||
{
|
||||
icon: 'fluent-emoji:balance-scale',
|
||||
title: '账户余额',
|
||||
totalTitle: '账户余额',
|
||||
totalValue: balance.value,
|
||||
value: balance.value,
|
||||
},
|
||||
{
|
||||
icon: 'game-icons:frozen-orb',
|
||||
title: '冻结金额(用户未确认收货)',
|
||||
totalTitle: '冻结金额(用户未确认收货)',
|
||||
totalValue: frozenAmount.value,
|
||||
value: frozenAmount.value,
|
||||
},
|
||||
{
|
||||
icon: 'fxemoji:hourglassflowingsand',
|
||||
title: '待结算收益',
|
||||
totalTitle: '待结算收益',
|
||||
totalValue: pendingEarnings.value,
|
||||
value: pendingEarnings.value,
|
||||
},
|
||||
{
|
||||
icon: 'fluent-emoji:alarm-clock',
|
||||
title: '审核中金额',
|
||||
totalTitle: '审核中金额',
|
||||
totalValue: withdrawnFrozenAmount.value,
|
||||
value: withdrawnFrozenAmount.value,
|
||||
},
|
||||
{
|
||||
icon: 'flat-color-icons:ok',
|
||||
title: '已提现金额',
|
||||
totalTitle: '已提现金额',
|
||||
totalValue: withdrawnAmount.value,
|
||||
value: withdrawnAmount.value,
|
||||
},
|
||||
// {
|
||||
// icon: SvgCardIcon,
|
||||
// title: '已提现金额',
|
||||
// totalTitle: '已提现金额',
|
||||
// totalValue: withdrawnAmount.value,
|
||||
// value: withdrawnAmount.value,
|
||||
// },
|
||||
// {
|
||||
// icon: 'flat-color-icons:ok',
|
||||
// title: '到账金额',
|
||||
// totalTitle: '到账金额',
|
||||
// totalValue: arrivedAmount.value,
|
||||
// value: arrivedAmount.value,
|
||||
// },
|
||||
// {
|
||||
// icon: 'icon-park:flash-payment',
|
||||
// title: '手续费',
|
||||
// totalTitle: '手续费',
|
||||
// totalValue: fee.value,
|
||||
// value: fee.value,
|
||||
// },
|
||||
];
|
||||
});
|
||||
}
|
||||
function showMyCard() {
|
||||
getMyCard().then((res) => {
|
||||
res.user_name = res?.user_name || res?.store?.name || res?.supplier?.name || res?.platform?.name;
|
||||
myCard.value = res;
|
||||
});
|
||||
}
|
||||
showCard();
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
const [saveMyCardModal, saveMyCardApi] = useVbenModal({
|
||||
connectedComponent: SaveMyCard,
|
||||
});
|
||||
|
||||
function refresh() {
|
||||
message.success('刷新成功');
|
||||
showCard();
|
||||
}
|
||||
|
||||
const showModal = (data = {}) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
balance,
|
||||
gridApi: props.gridApi,
|
||||
showCard,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
|
||||
const showCardModal = () => {
|
||||
saveMyCardApi.setData({
|
||||
// 表单值
|
||||
values: myCard.value,
|
||||
balance,
|
||||
gridApi: props.gridApi,
|
||||
showCard,
|
||||
});
|
||||
saveMyCardApi.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
:title="`${myCard?.user_name || myCard?.store?.name || myCard?.supplier?.name}--账户信息`"
|
||||
class="p-5"
|
||||
>
|
||||
<FormModal />
|
||||
<saveMyCardModal />
|
||||
<Button class="ml-5" type="primary" @click="showModal"> 申请提现 </Button>
|
||||
<Button class="ml-3" type="primary" @click="showCardModal">
|
||||
编辑账户
|
||||
</Button>
|
||||
<Button type="link" @click="refresh">刷新</Button>
|
||||
<AnalysisOverview :items="overviewItems" :my-card="myCard" class="mt-5" />
|
||||
</Card>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createWithdrawalApplicationWithdrawal } from '#/views/finance/withdrawal/api';
|
||||
|
||||
import { modalFormProps } from '../config/form';
|
||||
|
||||
const gridApi = ref();
|
||||
const showCard = ref();
|
||||
const maxAmount = ref(0);
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = createWithdrawalApplicationWithdrawal;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload();
|
||||
showCard.value();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
showCard.value = isOpen ? modalApi.getData()?.showCard : null;
|
||||
if (isOpen) {
|
||||
const { values, balance } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
maxAmount.value = balance;
|
||||
formApi.setValues(values);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const setAmount = (amount: number) => {
|
||||
formApi.setValues({ amount });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[30%]" title="申请提现">
|
||||
<Form />
|
||||
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Button type="primary" @click="setAmount(1000)"> 1000 </Button>
|
||||
<Button type="primary" @click="setAmount(3000)"> 3000 </Button>
|
||||
<Button type="primary" @click="setAmount(5000)"> 5000 </Button>
|
||||
<Button type="primary" @click="setAmount(10000)"> 10000 </Button>
|
||||
<Button type="primary" @click="setAmount(30000)"> 30000 </Button>
|
||||
<Button type="primary" @click="setAmount(50000)"> 50000 </Button>
|
||||
<Button type="primary" @click="setAmount(100000)"> 100000 </Button>
|
||||
<Button type="primary" @click="setAmount(maxAmount.value)">
|
||||
全部提现
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
172
apps/web-antd/src/views/finance/withdrawal/config/form.ts
Normal file
172
apps/web-antd/src/views/finance/withdrawal/config/form.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
export const modalFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// handleSubmit: onSubmit,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入提现金额',
|
||||
},
|
||||
fieldName: 'amount',
|
||||
label: '提现金额',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 银行卡编辑
|
||||
*/
|
||||
export const modalCardFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// handleSubmit: onSubmit,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
formItemClass: 'col-span-6',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户',
|
||||
disabled: true,
|
||||
},
|
||||
fieldName: 'user_name',
|
||||
label: '账户名称',
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return values.id != null;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户类型',
|
||||
options: [
|
||||
{ label: '诊所', value: 1 },
|
||||
{ label: '平台', value: 2 },
|
||||
{ label: '供应商', value: 3 },
|
||||
],
|
||||
disabled: true,
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '用户类型',
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return values.id != null;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入开户人姓名',
|
||||
},
|
||||
dependencies: {
|
||||
disabled: (values) => {
|
||||
return values.id != null;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
fieldName: 'bank_user_name',
|
||||
label: '开户人姓名',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入银行卡号',
|
||||
},
|
||||
dependencies: {
|
||||
disabled: (values) => {
|
||||
return values.id != null;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
fieldName: 'bank_card',
|
||||
label: '银行卡号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入银行名称',
|
||||
},
|
||||
dependencies: {
|
||||
disabled: (values) => {
|
||||
return values.id != null;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
fieldName: 'bank_name',
|
||||
label: '银行名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '对公', value: 1 },
|
||||
{ label: '对私', value: 2 },
|
||||
{ label: '存折', value: 5 },
|
||||
],
|
||||
placeholder: '请选择银行账户类型',
|
||||
},
|
||||
dependencies: {
|
||||
disabled: (values) => {
|
||||
return values.id != null;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
fieldName: 'bank_account_type',
|
||||
label: '银行账户类型',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: '请输入银行联行号',
|
||||
},
|
||||
dependencies: {
|
||||
disabled: (values) => {
|
||||
return values.id != null;
|
||||
},
|
||||
triggerFields: ['id'],
|
||||
},
|
||||
fieldName: 'bank_no',
|
||||
label: '银行联行号',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
67
apps/web-antd/src/views/finance/withdrawal/config/search.ts
Normal file
67
apps/web-antd/src/views/finance/withdrawal/config/search.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
placeholder: '选择审核状态',
|
||||
options: [
|
||||
{
|
||||
label: '审核中',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '已通过',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '已拒绝',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '提现失败',
|
||||
value: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'check_status',
|
||||
label: '审核状态',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
placeholder: '选择打款状态',
|
||||
options: [
|
||||
{
|
||||
label: '失败',
|
||||
value: -1,
|
||||
},
|
||||
{
|
||||
label: '未知',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '成功',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'dakuan_status',
|
||||
label: '打款状态',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
export const formOptions2: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
placeholder: '选择费用类型',
|
||||
options: [
|
||||
{
|
||||
label: '药品费用',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '挂号费用',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '快递费用',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '代煎费用',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: '加工费用',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: '诊疗费用',
|
||||
value: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'fee_type',
|
||||
label: '费用类型',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
placeholder: '选择结算状态',
|
||||
options: [
|
||||
{
|
||||
label: '待结算',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '已结算',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '已取消',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getSettlementList } from '#/views/finance/withdrawal/api/settlement';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
nick_name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
roles: { name: string }[];
|
||||
open_id: string;
|
||||
code: string;
|
||||
platform_id: string;
|
||||
phone: string;
|
||||
desc: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions2: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'order.order_no', title: '订单号' },
|
||||
{ field: 'user_id', title: '名称', slots: { default: 'user_id' } },
|
||||
{ field: 'money', title: '分账金额' },
|
||||
{ field: 'fee_type_txt', title: '费用类型' },
|
||||
{ field: 'status_txt', title: '结算状态' },
|
||||
{ field: 'created_at', title: '创建时间' },
|
||||
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getSettlementList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
// height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
84
apps/web-antd/src/views/finance/withdrawal/config/table.ts
Normal file
84
apps/web-antd/src/views/finance/withdrawal/config/table.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getWithdrawalApplicationList } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
nick_name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
roles: { name: string }[];
|
||||
open_id: string;
|
||||
code: string;
|
||||
platform_id: string;
|
||||
phone: string;
|
||||
desc: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'user_id', title: '名称', slots: { default: 'user_id' } },
|
||||
{ field: 'apply_cash', title: '申请金额' },
|
||||
{ field: 'true_cash', title: '实际到账金额' },
|
||||
{ field: 'charge_cash', title: '手续费' },
|
||||
{ field: 'apply_time', title: '申请时间' },
|
||||
{ field: 'check_id', title: '审核人', slots: { default: 'check_id' } },
|
||||
{ field: 'check_status', title: '审核状态', slots: { default: 'check_status'} },
|
||||
{ field: 'check_result', title: '审核结果', slots: { default: 'check_result'} },
|
||||
{ field: 'dakuan_status', title: '打款状态' },
|
||||
{ field: 'dakuan_time', title: '打款时间' },
|
||||
{ field: 'created_at', title: '注册时间' },
|
||||
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getWithdrawalApplicationList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
// scrollY: {
|
||||
// enabled: true,
|
||||
// gt: 100,
|
||||
// },
|
||||
// height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
113
apps/web-antd/src/views/finance/withdrawal/index.vue
Normal file
113
apps/web-antd/src/views/finance/withdrawal/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup>
|
||||
import { AnalysisChartsTabs, Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
import Statistics from './components/statistics.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { formOptions2 } from './config/settlement-search';
|
||||
import { gridOptions2 } from './config/settlement-table';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
const [Grid, GridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [Grid2] = useVbenVxeGrid({
|
||||
formOptions: formOptions2,
|
||||
gridOptions: gridOptions2,
|
||||
});
|
||||
|
||||
const chartTabs = [
|
||||
{
|
||||
label: '提现记录',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '结算记录',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
auto-content-height
|
||||
description="提现前请确认好您的打款账户"
|
||||
title="提现管理"
|
||||
>
|
||||
<Statistics :grid-api="GridApi" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<Grid>
|
||||
<template #toolbar-buttons></template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #user_id="{ row }">
|
||||
<span v-if="row.user_type === 1">{{ row.store.name }}</span>
|
||||
<span v-else-if="row.user_type === 2">萧康平台</span>
|
||||
<span v-else-if="row.user_type === 3">{{ row.supplier.name }}</span>
|
||||
</template>
|
||||
<template #check_status="{ row }">
|
||||
<Tag v-if="row.check_status === 1" color="blue">待审核</Tag>
|
||||
<Tag v-else-if="row.check_status === 2" color="green">审核成功</Tag>
|
||||
<Tag v-else-if="row.check_status === 3" color="red">拒绝</Tag>
|
||||
</template>
|
||||
<template #check_result="{ row }">
|
||||
<Tag v-if="row.check_status === 2" color="green">
|
||||
{{ row.check_result }}
|
||||
</Tag>
|
||||
<Tag v-else-if="row.check_status === 3" color="red">
|
||||
{{ row.check_result }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #check_id="{ row }">
|
||||
<span>{{ row.check_admin?.username || '暂无' }}</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
// {
|
||||
// label: '编辑',
|
||||
// type: 'link',
|
||||
// icon: 'uil:edit',
|
||||
// size: 'small',
|
||||
// onClick: showModal.bind(null, row, true),
|
||||
// },
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
<template #visits>
|
||||
<Grid2>
|
||||
<template #toolbar-buttons></template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #user_id="{ row }">
|
||||
<span v-if="row.user_type === 1">{{ row.store.name }}</span>
|
||||
<span v-else-if="row.user_type === 2">萧康平台</span>
|
||||
<span v-else-if="row.user_type === 3">{{ row.supplier.name }}</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
// {
|
||||
// label: '编辑',
|
||||
// type: 'link',
|
||||
// icon: 'uil:edit',
|
||||
// size: 'small',
|
||||
// // auth: ['admin', 'sys:role:detail'],
|
||||
// onClick: showModal.bind(null, row, true),
|
||||
// },
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
/>
|
||||
</template>
|
||||
</Grid2>
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
</Page>
|
||||
</template>
|
||||
11
apps/web-antd/src/views/log/api-log/api/index.ts
Normal file
11
apps/web-antd/src/views/log/api-log/api/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'log/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getOldApiLogList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}api-list`, { params: data });
|
||||
}
|
||||
// Api访问日志
|
||||
107
apps/web-antd/src/views/log/api-log/components/modal.vue
Normal file
107
apps/web-antd/src/views/log/api-log/components/modal.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
|
||||
import { Icon } from '#/components/icon';
|
||||
import { getIcon } from '#/util/tool';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const gridApi = ref();
|
||||
const data = ref({});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
data.value = values;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[80%]" title="Api访问日志详情">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Descriptions
|
||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="操作人员">
|
||||
{{ data?.admin?.nick_name || '获取失败' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="URL">{{ data.url }}</Descriptions.Item>
|
||||
<Descriptions.Item label="控制器">
|
||||
{{ data.controller }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="IP">
|
||||
{{ data.ip === '127.0.0.1' ? '本地' : data.ip }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">
|
||||
{{ data.type === 0 ? '访问成功' : '访问失败' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="返回结果代码">
|
||||
{{ data.result_code }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台类型">
|
||||
{{ data.platform_type === 0 ? '平台' : '诊所' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备">
|
||||
<Icon :icon="getIcon(data.equipment)" :size="20" />
|
||||
{{ data.equipment }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="浏览器">
|
||||
<Icon :icon="getIcon(data.browser)" :size="20" />
|
||||
{{ data.browser }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台">
|
||||
{{ data.platform?.name || '获取失败' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="访问时间">
|
||||
{{ data.created_at }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Descriptions
|
||||
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="参数">
|
||||
<pre>{{ JSON.parse(data.param) }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结果">
|
||||
<pre>{{ JSON.parse(data.result) }}</pre>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ant-descriptions-item-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
28
apps/web-antd/src/views/log/api-log/config/search.ts
Normal file
28
apps/web-antd/src/views/log/api-log/config/search.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD', // 确保格式与你需要的一致
|
||||
},
|
||||
// defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
fieldName: 'search_time',
|
||||
label: '时间范围',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
85
apps/web-antd/src/views/log/api-log/config/table.ts
Normal file
85
apps/web-antd/src/views/log/api-log/config/table.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getOldApiLogList } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
admin: string;
|
||||
url: string;
|
||||
ip: string;
|
||||
param: string;
|
||||
type: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
columns: [
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{
|
||||
field: 'admin',
|
||||
align: 'left',
|
||||
title: '操作管理员',
|
||||
slots: { default: 'admin' },
|
||||
},
|
||||
{ field: 'url', title: '访问路由' },
|
||||
{ field: 'ip', title: '用户IP地址' },
|
||||
{ field: 'controller', title: '访问控制器' },
|
||||
// { field: 'param', title: '携带参数' },
|
||||
{ field: 'equipment', title: '操作系统', slots: { default: 'equipment' }, width: 80 },
|
||||
{ field: 'browser', title: '浏览器', slots: { default: 'browser' }, width: 80 },
|
||||
{ field: 'type', title: '操作简述', slots: { default: 'type' } },
|
||||
{ field: 'created_at', title: '访问时间' },
|
||||
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOldApiLogList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
exportConfig: {
|
||||
// 导出配置
|
||||
filename: 'api-log',
|
||||
remote: true,
|
||||
type: 'csv',
|
||||
},
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
// import: true, // 导入
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
75
apps/web-antd/src/views/log/api-log/index.vue
Normal file
75
apps/web-antd/src/views/log/api-log/index.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
import { getIcon } from '#/util/tool';
|
||||
import {Icon} from "#/components/icon";
|
||||
|
||||
ref(false);
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const showModal = (data = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="Api访问日志管理">
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
</template>
|
||||
<template #type="{ row }">
|
||||
<Tag v-if="row.type === 0" color="green"> 访问成功 </Tag>
|
||||
<Tag v-else color="red"> 访问失败 </Tag>
|
||||
</template>
|
||||
<template #admin="{ row }">
|
||||
{{ row?.admin?.nick_name || '获取失败' }}
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #equipment="{ row }">
|
||||
<Icon :icon="getIcon(row.equipment)" :size="20" />
|
||||
</template>
|
||||
<template #browser="{ row }">
|
||||
<Icon :icon="getIcon(row.browser)" :size="20" />
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
// auth: ['op-log', 'sys:role:detail'],
|
||||
onClick: showModal.bind(null, row, true),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
10
apps/web-antd/src/views/log/ledger-log/api/index.ts
Normal file
10
apps/web-antd/src/views/log/ledger-log/api/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'log/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getLedgerLogList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}ledger-list`, { params: data });
|
||||
}
|
||||
70
apps/web-antd/src/views/log/ledger-log/components/modal.vue
Normal file
70
apps/web-antd/src/views/log/ledger-log/components/modal.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const gridApi = ref();
|
||||
const data = ref({});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
data.value = values;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-[80%]" title="分账日志详情">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Descriptions
|
||||
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="管理员ID">{{ data.admin_id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="理员名称">{{ data.old_admin.username }}</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">{{ data.type }}</Descriptions.Item>
|
||||
<Descriptions.Item label="内容">
|
||||
<pre>{{ JSON.parse(data.content) }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="模式">
|
||||
{{ data.mold_txt }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="操作时间">
|
||||
{{ data.operate_time }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{{ data.created_at }}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ant-descriptions-item-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
28
apps/web-antd/src/views/log/ledger-log/config/search.ts
Normal file
28
apps/web-antd/src/views/log/ledger-log/config/search.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD', // 确保格式与你需要的一致
|
||||
},
|
||||
// defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
fieldName: 'search_time',
|
||||
label: '时间范围',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
64
apps/web-antd/src/views/log/ledger-log/config/table.ts
Normal file
64
apps/web-antd/src/views/log/ledger-log/config/table.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getLedgerLogList } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
introduce: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columns: [
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'order.order_no', align: 'left', title: '订单编号' },
|
||||
{ field: 'store', title: '关联账户', slots: { default: 'store' } },
|
||||
{ field: 'user_type_txt', title: '账户类型' },
|
||||
{ field: 'fee_type_txt', title: '费用类型' },
|
||||
{ field: 'type_txt', title: '类型' },
|
||||
{ field: 'content', title: '操作内容' },
|
||||
{ field: 'amount', title: '金额' },
|
||||
{ field: 'created_at', title: '操作时间' },
|
||||
// { type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getLedgerLogList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
66
apps/web-antd/src/views/log/ledger-log/index.vue
Normal file
66
apps/web-antd/src/views/log/ledger-log/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
ref(false);
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const showModal = (data = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="分账日志管理">
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
</template>
|
||||
<template #store="{ row }">
|
||||
<span v-if="row.user_type === 1">{{ row.store.name }}</span>
|
||||
<span v-else-if="row.user_type === 3">{{ row.supplier.name }}</span>
|
||||
<span v-else>萧康-平台</span>
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
// auth: ['op-log', 'sys:role:detail'],
|
||||
onClick: showModal.bind(null, row, true),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
11
apps/web-antd/src/views/log/old-api-log/api/index.ts
Normal file
11
apps/web-antd/src/views/log/old-api-log/api/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'log/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getOldApiLogList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}old-api-list`, { params: data });
|
||||
}
|
||||
// Api访问日志
|
||||
86
apps/web-antd/src/views/log/old-api-log/components/modal.vue
Normal file
86
apps/web-antd/src/views/log/old-api-log/components/modal.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const gridApi = ref();
|
||||
const data = ref({});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
data.value = values;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[80%]" title="Api访问日志详情">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Descriptions
|
||||
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户ID">{{ data.user_id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="URL">{{ data.url }}</Descriptions.Item>
|
||||
<Descriptions.Item label="IP">{{ data.ip === '127.0.0.1' ? '本地' : data.ip }}</Descriptions.Item>
|
||||
<Descriptions.Item label="参数">
|
||||
<pre>{{ JSON.parse(JSON.parse(data.param)) }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结果">
|
||||
<pre>{{ JSON.parse(JSON.parse(data.result)) }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">{{ data.type === 0 ? '访问成功' : '访问失败' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="返回结果代码">
|
||||
{{ data.result_code }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台类型">
|
||||
{{ data.platform_type === 0 ? '平台' : '诊所' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台">
|
||||
{{ data.platform_id === 0 ? '萧康医药' : data?.old_admin?.username || '获取失败' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备">{{ data.equipment }}</Descriptions.Item>
|
||||
<Descriptions.Item label="浏览器">{{ data.browser }}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ data.created_at }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="管理员名称">
|
||||
{{ data?.old_admin?.username || '获取失败' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ant-descriptions-item-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
28
apps/web-antd/src/views/log/old-api-log/config/search.ts
Normal file
28
apps/web-antd/src/views/log/old-api-log/config/search.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD', // 确保格式与你需要的一致
|
||||
},
|
||||
// defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
fieldName: 'search_time',
|
||||
label: '时间范围',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
73
apps/web-antd/src/views/log/old-api-log/config/table.ts
Normal file
73
apps/web-antd/src/views/log/old-api-log/config/table.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getOldApiLogList } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
introduce: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
columns: [
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{
|
||||
field: 'old_admin',
|
||||
align: 'left',
|
||||
title: '操作管理员',
|
||||
slots: { default: 'old_admin' },
|
||||
},
|
||||
{ field: 'url', title: '访问路由' },
|
||||
{ field: 'ip', title: '用户IP地址' },
|
||||
{ field: 'param', title: '携带参数' },
|
||||
{ field: 'type', title: '操作简述', slots: { default: 'type' } },
|
||||
{ field: 'created_at', title: '上传时间' },
|
||||
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOldApiLogList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
68
apps/web-antd/src/views/log/old-api-log/index.vue
Normal file
68
apps/web-antd/src/views/log/old-api-log/index.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
ref(false);
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const showModal = (data = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="Api访问日志管理">
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
</template>
|
||||
<template #type="{ row }">
|
||||
<Tag v-if="row.type === 0" color="green"> 访问成功 </Tag>
|
||||
<Tag v-else color="red"> 访问失败 </Tag>
|
||||
</template>
|
||||
<template #old_admin="{ row }">
|
||||
{{ row?.old_admin?.username || '获取失败' }}
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
// auth: ['op-log', 'sys:role:detail'],
|
||||
onClick: showModal.bind(null, row, true),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
10
apps/web-antd/src/views/log/op-log/api/index.ts
Normal file
10
apps/web-antd/src/views/log/op-log/api/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'log/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getOpLogList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}op-list`, { params: data });
|
||||
}
|
||||
71
apps/web-antd/src/views/log/op-log/components/modal.vue
Normal file
71
apps/web-antd/src/views/log/op-log/components/modal.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormModelDemo',
|
||||
});
|
||||
|
||||
const gridApi = ref();
|
||||
const data = ref({});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
data.value = values;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-[80%]" title="操作日志详情">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Descriptions
|
||||
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="ID">{{ data.id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="管理员ID">{{ data.admin_id }}</Descriptions.Item>
|
||||
<Descriptions.Item label="理员名称">{{ data.old_admin?.username || data.admin?.nick_name }}</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">{{ data.type }}</Descriptions.Item>
|
||||
<Descriptions.Item label="内容">
|
||||
<pre>{{ JSON.parse(data.content) }}</pre>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="模式">
|
||||
{{ data.mold_txt }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="操作时间">
|
||||
{{ data.operate_time }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{{ data.created_at }}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ant-descriptions-item-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
28
apps/web-antd/src/views/log/op-log/config/search.ts
Normal file
28
apps/web-antd/src/views/log/op-log/config/search.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
// import dayjs from 'dayjs';
|
||||
|
||||
export const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD', // 确保格式与你需要的一致
|
||||
},
|
||||
// defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
fieldName: 'search_time',
|
||||
label: '时间范围',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: true,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
73
apps/web-antd/src/views/log/op-log/config/table.ts
Normal file
73
apps/web-antd/src/views/log/op-log/config/table.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getOpLogList } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
introduce: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
columns: [
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{
|
||||
field: 'old_admin',
|
||||
align: 'left',
|
||||
title: '操作管理员',
|
||||
slots: { default: 'old_admin' },
|
||||
},
|
||||
{ field: 'type', title: '操作简述' },
|
||||
{ field: 'mold_txt', title: '操作类型' },
|
||||
{ field: 'content', title: '操作内容', slots: { default: 'content'} },
|
||||
{ field: 'operate_time', title: '操作时间' },
|
||||
{ field: 'created_at', title: '上传时间' },
|
||||
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOpLogList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
84
apps/web-antd/src/views/log/op-log/index.vue
Normal file
84
apps/web-antd/src/views/log/op-log/index.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
|
||||
ref(false);
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const showModal = (data = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="操作日志管理">
|
||||
<FormModal />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:actions="[
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
]"
|
||||
>
|
||||
<template #more>
|
||||
<Button style="margin-left: 16px">
|
||||
批量操作
|
||||
<Icon icon="ant-design:down-outlined" />
|
||||
</Button>
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #old_admin="{ row }">
|
||||
{{ row.old_admin?.username || row.admin?.nick_name }}
|
||||
</template>
|
||||
<template #content="{ row }">
|
||||
{{
|
||||
row.content.length > 100
|
||||
? `${row.content.slice(0, 100)}...`
|
||||
: row.content
|
||||
}}
|
||||
</template>
|
||||
<template #toolbar-tools></template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看',
|
||||
type: 'link',
|
||||
icon: 'uil:edit',
|
||||
size: 'small',
|
||||
// auth: ['op-log', 'sys:role:detail'],
|
||||
onClick: showModal.bind(null, row, true),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
10
apps/web-antd/src/views/log/order-log/api/index.ts
Normal file
10
apps/web-antd/src/views/log/order-log/api/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'log/';
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
*/
|
||||
export async function getOrderLogList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}order-list`, { params: data });
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user