Merge remote-tracking branch 'refs/remotes/yudao/dev' into develop

This commit is contained in:
puhui999
2025-05-03 13:49:52 +08:00
283 changed files with 10304 additions and 3435 deletions

View File

@@ -1,17 +1,20 @@
<script setup lang="ts">
import type { SystemUserProfileApi } from '#/api/system/user/profile';
import { Card, Tabs } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import ProfileUser from './modules/profile-user.vue';
import BaseInfo from './modules/base-info.vue';
import ResetPwd from './modules/reset-pwd.vue';
import UserSocial from './modules/user-social.vue';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Tabs } from 'ant-design-vue';
import { getUserProfile } from '#/api/system/user/profile';
import { useAuthStore } from '#/store';
import BaseInfo from './modules/base-info.vue';
import ProfileUser from './modules/profile-user.vue';
import ResetPwd from './modules/reset-pwd.vue';
import UserSocial from './modules/user-social.vue';
const authStore = useAuthStore();
const activeName = ref('basicInfo');
@@ -46,13 +49,13 @@ onMounted(loadProfile);
<Card class="ml-3 w-3/5">
<Tabs v-model:active-key="activeName" class="-mt-4">
<Tabs.TabPane key="basicInfo" tab="基本设置">
<BaseInfo :profile="profile" @success="refreshProfile" />
<BaseInfo :profile="profile" @success="refreshProfile" />
</Tabs.TabPane>
<Tabs.TabPane key="resetPwd" tab="密码设置">
<ResetPwd />
<ResetPwd />
</Tabs.TabPane>
<Tabs.TabPane key="userSocial" tab="社交绑定" force-render>
<UserSocial @update:active-name="activeName = $event" />
<UserSocial @update:active-name="activeName = $event" />
</Tabs.TabPane>
<!-- TODO @芋艿在线设备 -->
</Tabs>

View File

@@ -1,16 +1,21 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { SystemUserProfileApi } from '#/api/system/user/profile';
import { watch } from 'vue';
import { $t } from '@vben/locales';
import { message } from 'ant-design-vue';
import { watch } from 'vue';
import { useVbenForm, z } from '#/adapter/form';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { updateUserProfile } from '#/api/system/user/profile';
import { $t } from '@vben/locales';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
const props = defineProps<{ profile?: SystemUserProfileApi.UserProfileRespVO }>();
const props = defineProps<{
profile?: SystemUserProfileApi.UserProfileRespVO;
}>();
const emit = defineEmits<{
(e: 'success'): void;
}>();
@@ -87,11 +92,15 @@ async function handleSubmit(values: Recordable<any>) {
}
/** 监听 profile 变化 */
watch(() => props.profile, (newProfile) => {
if (newProfile) {
formApi.setValues(newProfile);
}
}, { immediate: true });
watch(
() => props.profile,
(newProfile) => {
if (newProfile) {
formApi.setValues(newProfile);
}
},
{ immediate: true },
);
</script>
<template>

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import { $t } from '@vben/locales';
import { message } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter/form';
import { updateUserPassword } from '#/api/system/user/profile';
import { $t } from '@vben/locales';
const [Form, formApi] = useVbenForm({
commonConfig: {

View File

@@ -2,26 +2,27 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemSocialUserApi } from '#/api/system/social/user';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { Button, Card, Image, message, Modal } from 'ant-design-vue';
import { computed, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { socialAuthRedirect } from '#/api/core/auth';
import {
getBindSocialUserList,
socialUnbind,
socialBind,
socialUnbind,
} from '#/api/system/social/user';
import { socialAuthRedirect } from '#/api/core/auth';
import { DICT_TYPE, getDictLabel } from '#/utils/dict';
import { $t } from '#/locales';
import { SystemUserSocialTypeEnum } from '#/utils/constants';
import { DICT_TYPE, getDictLabel } from '#/utils/dict';
const route = useRoute();
const emit = defineEmits<{
(e: 'update:activeName', v: string): void;
}>();
const route = useRoute();
/** 已经绑定的平台 */
const bindList = ref<SystemSocialUserApi.SocialUser[]>([]);
const allBindList = computed<any[]>(() => {
@@ -126,8 +127,7 @@ async function onBind(bind: any) {
try {
// 计算 redirectUri
// tricky: type 需要先 encode 一次,否则钉钉回调会丢失。配合 getUrlValue() 使用
const redirectUri =
location.origin + '/profile?' + encodeURIComponent(`type=${type}`);
const redirectUri = `${location.origin}/profile?${encodeURIComponent(`type=${type}`)}`;
// 进行跳转
window.location.href = await socialAuthRedirect(type, redirectUri);

View File

@@ -63,7 +63,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
class: 'w-full',
controlsPosition: 'right',
placeholder: '请输入分类排序',
},

View File

@@ -50,7 +50,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -69,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="审批接入(流程表单)"
url="https://doc.iocoder.cn/bpm/use-bpm-form/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,28 +1,242 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type { BpmModelApi } from '#/api/bpm/model';
import { Button } from 'ant-design-vue';
import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { Plus, Search, Settings } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { refAutoReset } from '@vueuse/core';
import { useSortable } from '@vueuse/integrations/useSortable';
import {
Button,
Card,
Divider,
Dropdown,
Form,
Input,
Menu,
message,
} from 'ant-design-vue';
import {
getCategorySimpleList,
updateCategorySortBatch,
} from '#/api/bpm/category';
import { getModelList } from '#/api/bpm/model';
import CategoryDraggableModel from './modules/category-draggable-model.vue';
// 模型列表加载状态
const modelListSpinning = refAutoReset(false, 3000);
// 保存排序状态
const saveSortLoading = ref(false);
// 按照 category 分组的数据
const categoryGroup = ref<BpmModelApi.ModelCategoryInfo[]>([]);
// 未排序前的原始数据
const originalData = ref<BpmModelApi.ModelCategoryInfo[]>([]);
// 可以排序元素的容器
const sortable = useTemplateRef<HTMLElement>('categoryGroupRef');
// 排序引用,以便后续启用或禁用排序
const sortableInstance = ref<any>(null);
// 分类排序状态
const isCategorySorting = ref(false);
// 查询参数
const queryParams = reactive({
name: '',
});
// 监听分类排序模式切换
watch(
() => isCategorySorting.value,
(newValue) => {
if (sortableInstance.value) {
if (newValue) {
// 启用排序功能
sortableInstance.value.option('disabled', false);
} else {
// 禁用排序功能
sortableInstance.value.option('disabled', true);
}
}
},
);
/** 加载数据 */
const getList = async () => {
modelListSpinning.value = true;
try {
const modelList = await getModelList(queryParams.name);
const categoryList = await getCategorySimpleList();
// 按照 category 聚合
categoryGroup.value = categoryList.map((category: any) => ({
...category,
modelList: modelList.filter(
(model: any) => model.categoryName === category.name,
),
}));
// 重置排序实例
sortableInstance.value = null;
} finally {
modelListSpinning.value = false;
}
};
/** 初始化 */
onActivated(() => {
getList();
});
/** 查询方法 */
const handleQuery = () => {
getList();
};
/** 新增模型 */
const createModel = () => {
// TODO 新增模型
};
/** 处理下拉菜单命令 */
const handleCommand = (command: string) => {
if (command === 'handleCategoryAdd') {
// TODO 新建分类逻辑
} else if (command === 'handleCategorySort') {
originalData.value = cloneDeep(categoryGroup.value);
isCategorySorting.value = true;
// 如果排序实例不存在,则初始化
if (sortableInstance.value) {
// 已存在实例,则启用排序功能
sortableInstance.value.option('disabled', false);
} else {
sortableInstance.value = useSortable(sortable, categoryGroup, {
disabled: false, // 启用排序
});
}
}
};
/** 取消分类排序 */
const handleCategorySortCancel = () => {
// 恢复初始数据
categoryGroup.value = cloneDeep(originalData.value);
isCategorySorting.value = false;
// 直接禁用排序功能
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
/** 提交分类排序 */
const handleCategorySortSubmit = async () => {
saveSortLoading.value = true;
try {
// 保存排序逻辑
const ids = categoryGroup.value.map((item: any) => item.id);
await updateCategorySortBatch(ids);
} finally {
saveSortLoading.value = false;
}
message.success('分类排序成功');
isCategorySorting.value = false;
// 刷新列表
await getList();
// 禁用排序功能
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
</script>
<template>
<Page>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
<Page auto-content-height>
<Card
:body-style="{ padding: '10px' }"
class="mb-4"
v-spinning="modelListSpinning"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/model/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/model/index
代码pull request 贡献给我们
</Button>
<div class="flex items-center justify-between pl-5">
<span class="-mb-4 text-lg font-extrabold">流程模型</span>
<!-- 搜索工作栏 -->
<Form
v-if="!isCategorySorting"
class="-mb-4 mr-2.5 flex"
:model="queryParams"
layout="inline"
>
<Form.Item name="name" class="ml-auto">
<Input
v-model:value="queryParams.name"
placeholder="搜索流程"
allow-clear
@press-enter="handleQuery"
class="!w-60"
>
<template #prefix>
<Search class="mx-2.5" />
</template>
</Input>
</Form.Item>
<!-- 右上角新建模型更多操作 -->
<Form.Item>
<Button type="primary" @click="createModel">
<Plus class="size-5" /> 新建模型
</Button>
</Form.Item>
<Form.Item>
<Dropdown placement="bottomRight">
<Button>
<template #icon>
<Settings class="size-4" />
</template>
</Button>
<template #overlay>
<Menu @click="(e) => handleCommand(e.key as string)">
<Menu.Item key="handleCategoryAdd">
<div class="flex items-center">
<span
class="icon-[ant-design--plus-outlined] mr-1.5 text-[18px]"
></span>
新建分类
</div>
</Menu.Item>
<Menu.Item key="handleCategorySort">
<div class="flex items-center">
<span class="icon-[fa--sort-amount-desc] mr-1.5"></span>
分类排序
</div>
</Menu.Item>
</Menu>
</template>
</Dropdown>
</Form.Item>
</Form>
<div class="-mb-4 mr-6" v-else>
<Button @click="handleCategorySortCancel" class="mr-3">
</Button>
<Button
type="primary"
:loading="saveSortLoading"
@click="handleCategorySortSubmit"
>
保存排序
</Button>
</div>
</div>
<Divider />
<!-- 按照分类展示其所属的模型列表 -->
<div class="px-5" ref="categoryGroupRef">
<CategoryDraggableModel
v-for="element in categoryGroup"
:class="isCategorySorting ? 'cursor-move' : ''"
:key="element.id"
:category-info="element"
:is-category-sorting="isCategorySorting"
@success="getList"
/>
</div>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,398 @@
<script lang="ts" setup>
import type { BpmCategoryApi } from '#/api/bpm/category';
import type { BpmModelApi } from '#/api/bpm/model';
import { computed, ref, watchEffect } from 'vue';
import { cloneDeep, formatDateTime, isEqual } from '@vben/utils';
import { useDebounceFn } from '@vueuse/core';
import { useSortable } from '@vueuse/integrations/useSortable';
import {
Button,
Card,
Collapse,
message,
Table,
Tag,
Tooltip,
} from 'ant-design-vue';
import { updateModelSortBatch } from '#/api/bpm/model';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
const props = defineProps<{
categoryInfo: BpmCategoryApi.ModelCategoryInfo;
isCategorySorting: boolean;
}>();
const emit = defineEmits(['success']);
const isModelSorting = ref(false);
const originalData = ref<BpmModelApi.ModelVO[]>([]);
const modelList = ref<BpmModelApi.ModelVO[]>([]);
const isExpand = ref(false);
const tableRef = ref();
// 排序引用,以便后续启用或禁用排序
const sortableInstance = ref<any>(null);
/** 解决 v-model 问题,使用计算属性 */
const expandKeys = computed(() => (isExpand.value ? ['1'] : []));
// 表格列配置
const columns = [
{
title: '流程名',
dataIndex: 'name',
key: 'name',
align: 'left' as const,
minWidth: 250,
},
{
title: '可见范围',
dataIndex: 'startUserIds',
key: 'startUserIds',
align: 'center' as const,
minWidth: 150,
},
{
title: '流程类型',
dataIndex: 'type',
key: 'type',
align: 'center' as const,
minWidth: 120,
},
{
title: '表单信息',
dataIndex: 'formType',
key: 'formType',
align: 'center' as const,
minWidth: 150,
},
{
title: '最后发布',
dataIndex: 'deploymentTime',
key: 'deploymentTime',
align: 'center' as const,
minWidth: 250,
},
{
title: '操作',
key: 'operation',
align: 'center' as const,
fixed: 'right' as const,
width: 150,
},
];
/** 处理模型的排序 */
const handleModelSort = () => {
// 保存初始数据
originalData.value = cloneDeep(props.categoryInfo.modelList);
// 展开数据
isExpand.value = true;
isModelSorting.value = true;
// 如果排序实例不存在,则初始化
if (sortableInstance.value) {
// 已存在实例,则启用排序功能
sortableInstance.value.option('disabled', false);
} else {
const sortableClass = `.category-${props.categoryInfo.id} .ant-table .ant-table-tbody`;
sortableInstance.value = useSortable(sortableClass, modelList, {
disabled: false, // 启用排序
});
}
};
/** 处理模型的排序提交 */
const handleModelSortSubmit = async () => {
try {
// 保存排序
const ids = modelList.value.map((item) => item.id);
await updateModelSortBatch(ids);
// 刷新列表
isModelSorting.value = false;
message.success('排序模型成功');
emit('success');
} catch (error) {
console.error('排序保存失败', error);
}
};
/** 处理模型的排序取消 */
const handleModelSortCancel = () => {
// 恢复初始数据
modelList.value = cloneDeep(originalData.value);
isModelSorting.value = false;
// 禁用排序功能
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
/** 处理表单详情点击 */
const handleFormDetail = (row: any) => {
// TODO 待实现
console.warn('待实现', row);
};
/** 更新 modelList 模型列表 */
const updateModelList = useDebounceFn(() => {
const newModelList = props.categoryInfo.modelList;
if (!isEqual(modelList.value, newModelList)) {
modelList.value = cloneDeep(newModelList);
if (newModelList?.length > 0) {
isExpand.value = true;
}
// 关闭排序
isModelSorting.value = false;
// 重置排序实例
sortableInstance.value = null;
}
}, 100);
/** 监听分类信息和排序状态变化 */
watchEffect(() => {
if (props.categoryInfo?.modelList) {
updateModelList();
}
if (props.isCategorySorting) {
isExpand.value = false;
}
});
/** 自定义表格行渲染 */
const customRow = (_record: any) => {
return {
class: isModelSorting.value ? 'cursor-move' : '',
};
};
</script>
<template>
<Card
:body-style="{ padding: 0 }"
class="category-draggable-model mb-5 rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
>
<div class="flex h-12 items-center">
<!-- 头部分类名 -->
<div class="flex items-center">
<Tooltip v-if="isCategorySorting" title="拖动排序">
<span
class="icon-[ic--round-drag-indicator] ml-2.5 cursor-move text-2xl text-gray-500"
></span>
</Tooltip>
<div class="ml-4 mr-2 text-lg font-medium">{{ categoryInfo.name }}</div>
<div class="text-gray-500">
({{ categoryInfo.modelList?.length || 0 }})
</div>
</div>
<!-- 头部操作 -->
<div class="flex flex-1 items-center" v-show="!isCategorySorting">
<div
v-if="categoryInfo.modelList.length > 0"
class="ml-3 flex cursor-pointer items-center transition-transform duration-300"
:class="isExpand ? 'rotate-180' : 'rotate-0'"
@click="isExpand = !isExpand"
>
<span
class="icon-[ic--round-expand-more] text-3xl text-gray-400"
></span>
</div>
<div
class="ml-auto flex items-center"
:class="isModelSorting ? 'mr-4' : 'mr-12'"
>
<template v-if="!isModelSorting">
<Button
v-if="categoryInfo.modelList.length > 0"
type="link"
class="mr-5 flex items-center text-[14px]"
@click.stop="handleModelSort"
>
<template #icon>
<span class="icon-[fa--sort-amount-desc] mr-1"></span>
</template>
排序
</Button>
</template>
<template v-else>
<Button @click.stop="handleModelSortCancel" class="mr-2">
</Button>
<Button type="primary" @click.stop="handleModelSortSubmit">
保存排序
</Button>
</template>
</div>
</div>
</div>
<!-- 模型列表 -->
<Collapse :active-key="expandKeys" :bordered="false" class="bg-transparent">
<Collapse.Panel
key="1"
:show-arrow="false"
class="border-0 bg-transparent p-0"
v-show="isExpand"
>
<Table
v-if="modelList && modelList.length > 0"
:class="`category-${categoryInfo.id}`"
ref="tableRef"
:data-source="modelList"
:columns="columns"
:pagination="false"
:custom-row="customRow"
row-key="id"
>
<template #bodyCell="{ column, record }">
<!-- 流程名 -->
<template v-if="column.key === 'name'">
<div class="flex items-center">
<Tooltip v-if="isModelSorting" title="拖动排序">
<span
class="icon-[ic--round-drag-indicator] mr-2.5 cursor-move text-2xl text-gray-500"
></span>
</Tooltip>
<div
v-if="!record.icon"
class="mr-2.5 flex h-9 w-9 items-center justify-center rounded bg-blue-500 text-white"
>
<span style="font-size: 12px">{{
record.name.substring(0, 2)
}}</span>
</div>
<img
v-else
:src="record.icon"
class="mr-2.5 h-9 w-9 rounded"
alt="图标"
/>
{{ record.name }}
</div>
</template>
<!-- 可见范围列-->
<template v-else-if="column.key === 'startUserIds'">
<span
v-if="!record.startUsers?.length && !record.startDepts?.length"
>
全部可见
</span>
<span v-else-if="record.startUsers?.length === 1">
{{ record.startUsers[0].nickname }}
</span>
<span v-else-if="record.startDepts?.length === 1">
{{ record.startDepts[0].name }}
</span>
<span v-else-if="record.startDepts?.length > 1">
<Tooltip
placement="top"
:title="
record.startDepts.map((dept: any) => dept.name).join('、')
"
>
{{ record.startDepts[0].name }}
{{ record.startDepts.length }} 个部门可见
</Tooltip>
</span>
<span v-else-if="record.startUsers?.length > 1">
<Tooltip
placement="top"
:title="
record.startUsers
.map((user: any) => user.nickname)
.join('、')
"
>
{{ record.startUsers[0].nickname }}
{{ record.startUsers.length }} 人可见
</Tooltip>
</span>
</template>
<!-- 流程类型列 -->
<template v-else-if="column.key === 'type'">
<!-- <DictTag :value="record.type" :type="DICT_TYPE.BPM_MODEL_TYPE" /> -->
<!-- <Tag>{{ record.type }}</Tag> -->
<DictTag :type="DICT_TYPE.BPM_MODEL_TYPE" :value="record.type" />
</template>
<!-- 表单信息列 -->
<template v-else-if="column.key === 'formType'">
<!-- TODO BpmModelFormType.NORMAL -->
<Button
v-if="record.formType === 10"
type="link"
@click="handleFormDetail(record)"
>
{{ record.formName }}
</Button>
<!-- TODO BpmModelFormType.CUSTOM -->
<Button
v-else-if="record.formType === 20"
type="link"
@click="handleFormDetail(record)"
>
{{ record.formCustomCreatePath }}
</Button>
<span v-else>暂无表单</span>
</template>
<!-- 最后发布列 -->
<template v-else-if="column.key === 'deploymentTime'">
<div class="flex items-center justify-center">
<span v-if="record.processDefinition" class="w-[150px]">
{{ formatDateTime(record.processDefinition.deploymentTime) }}
</span>
<Tag v-if="record.processDefinition">
v{{ record.processDefinition.version }}
</Tag>
<Tag v-else color="warning">未部署</Tag>
<Tag
v-if="record.processDefinition?.suspensionState === 2"
color="warning"
class="ml-[10px]"
>
已停用
</Tag>
</div>
</template>
<!-- 操作列 -->
<template v-else-if="column.key === 'operation'">
<div class="flex items-center justify-center">待实现</div>
</template>
</template>
</Table>
</Collapse.Panel>
</Collapse>
</Card>
</template>
<style lang="scss" scoped>
.category-draggable-model {
// ant-table-tbody 自定义样式
:deep(.ant-table-tbody > tr > td) {
overflow: hidden;
border-bottom: none;
}
// ant-collapse-header 自定义样式
:deep(.ant-collapse-header) {
padding: 0;
}
// 优化表格渲染性能
:deep(.ant-table-tbody) {
transform: translateZ(0);
will-change: transform;
}
// 折叠面板样式
:deep(.ant-collapse-content-box) {
padding: 0;
}
}
</style>

View File

@@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="审批接入(业务表单)"
url="https://doc.iocoder.cn/bpm/use-business-form/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="流程发起、取消、重新发起"
url="https://doc.iocoder.cn/bpm/process-instance/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="执行监听器、任务监听器"
url="https://doc.iocoder.cn/bpm/listener/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,18 +1,34 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<DocAlert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,21 +1,40 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
<DocAlert
title="审批通过、不通过、驳回"
url="https://doc.iocoder.cn/bpm/task-todo-done/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" />
<DocAlert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,18 +1,31 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -1,21 +1,40 @@
<script lang="ts" setup>
import { DocAlert } from '#/components/doc-alert';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
</script>
<template>
<Page>
<DocAlert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
<DocAlert
title="审批通过、不通过、驳回"
url="https://doc.iocoder.cn/bpm/task-todo-done/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" />
<DocAlert
title="审批转办、委派、抄送"
url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
/>
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
<Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3">
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index">
可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index 代码pull request 贡献给我们!
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index
代码pull request 贡献给我们
</Button>
</Page>
</template>
</template>

View File

@@ -0,0 +1,895 @@
import type { Ref } from 'vue';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import type { CrmReceivableApi } from '#/api/crm/receivable';
import { useAccess } from '@vben/access';
import { DICT_TYPE } from '#/utils/dict';
const { hasAccessByCodes } = useAccess();
export interface LeftSideItem {
name: string;
menu: string;
count: Ref<number>;
}
/** 跟进状态 */
export const FOLLOWUP_STATUS = [
{ label: '待跟进', value: false },
{ label: '已跟进', value: true },
];
/** 归属范围 */
export const SCENE_TYPES = [
{ label: '我负责的', value: 1 },
{ label: '我参与的', value: 2 },
{ label: '下属负责的', value: 3 },
];
/** 联系状态 */
export const CONTACT_STATUS = [
{ label: '今日需联系', value: 1 },
{ label: '已逾期', value: 2 },
{ label: '已联系', value: 3 },
];
/** 审批状态 */
export const AUDIT_STATUS = [
{ label: '待审批', value: 10 },
{ label: '审核通过', value: 20 },
{ label: '审核不通过', value: 30 },
];
/** 回款提醒类型 */
export const RECEIVABLE_REMIND_TYPE = [
{ label: '待回款', value: 1 },
{ label: '已逾期', value: 2 },
{ label: '已回款', value: 3 },
];
/** 合同过期状态 */
export const CONTRACT_EXPIRY_TYPE = [
{ label: '即将过期', value: 1 },
{ label: '已过期', value: 2 },
];
export const useLeftSides = (
customerTodayContactCount: Ref<number>,
clueFollowCount: Ref<number>,
customerFollowCount: Ref<number>,
customerPutPoolRemindCount: Ref<number>,
contractAuditCount: Ref<number>,
contractRemindCount: Ref<number>,
receivableAuditCount: Ref<number>,
receivablePlanRemindCount: Ref<number>,
): LeftSideItem[] => {
return [
{
name: '今日需联系客户',
menu: 'customerTodayContact',
count: customerTodayContactCount,
},
{
name: '分配给我的线索',
menu: 'clueFollow',
count: clueFollowCount,
},
{
name: '分配给我的客户',
menu: 'customerFollow',
count: customerFollowCount,
},
{
name: '待进入公海的客户',
menu: 'customerPutPoolRemind',
count: customerPutPoolRemindCount,
},
{
name: '待审核合同',
menu: 'contractAudit',
count: contractAuditCount,
},
{
name: '待审核回款',
menu: 'receivableAudit',
count: receivableAuditCount,
},
{
name: '待回款提醒',
menu: 'receivablePlanRemind',
count: receivablePlanRemindCount,
},
{
name: '即将到期的合同',
menu: 'contractRemind',
count: contractRemindCount,
},
];
};
/** 分配给我的线索 列表的搜索表单 */
export function useClueFollowFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'followUpStatus',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: FOLLOWUP_STATUS,
},
defaultValue: false,
},
];
}
/** 分配给我的线索 列表的字段 */
export function useClueFollowColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '线索名称',
minWidth: 160,
fixed: 'left',
slots: { default: 'name' },
},
{
field: 'source',
title: '线索来源',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
},
},
{
field: 'mobile',
title: '手机',
minWidth: 120,
},
{
field: 'telephone',
title: '电话',
minWidth: 130,
},
{
field: 'email',
title: '邮箱',
minWidth: 180,
},
{
field: 'detailAddress',
title: '地址',
minWidth: 180,
},
{
field: 'industryId',
title: '客户行业',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
},
},
{
field: 'level',
title: '客户级别',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
},
},
{
field: 'contactNextTime',
title: '下次联系时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'contactLastTime',
title: '最后跟进时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'contactLastContent',
title: '最后跟进记录',
minWidth: 200,
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 100,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
];
}
/** 合同审核列表的搜索表单 */
export function useContractAuditFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'auditStatus',
label: '合同状态',
component: 'Select',
componentProps: {
allowClear: true,
options: AUDIT_STATUS,
},
defaultValue: 10,
},
];
}
/** 合同提醒列表的搜索表单 */
export function useContractRemindFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'expiryType',
label: '到期状态',
component: 'Select',
componentProps: {
allowClear: true,
options: CONTRACT_EXPIRY_TYPE,
},
defaultValue: 1,
},
];
}
/** 合同审核列表的字段 */
export function useContractColumns<T = CrmContractApi.Contract>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'no',
title: '合同编号',
minWidth: 160,
fixed: 'left',
},
{
field: 'name',
title: '合同名称',
minWidth: 160,
slots: {
default: 'name',
},
},
{
field: 'customerName',
title: '客户名称',
minWidth: 160,
slots: {
default: 'customerName',
},
},
{
field: 'businessName',
title: '商机名称',
minWidth: 160,
slots: {
default: 'businessName',
},
},
{
field: 'price',
title: '合同金额(元)',
minWidth: 120,
formatter: 'formatAmount',
},
{
field: 'orderDate',
title: '下单时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'startTime',
title: '合同开始时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'endTime',
title: '合同结束时间',
minWidth: 120,
formatter: 'formatDateTime',
},
{
field: 'contactName',
title: '客户签约人',
minWidth: 130,
slots: {
default: 'contactName',
},
},
{
field: 'signUserName',
title: '公司签约人',
minWidth: 130,
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'totalReceivablePrice',
title: '已回款金额(元)',
minWidth: 140,
formatter: 'formatAmount',
},
{
field: 'noReceivablePrice',
title: '未回款金额(元)',
minWidth: 120,
formatter: 'formatAmount',
},
{
field: 'contactLastTime',
title: '最后跟进时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 120,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
{
field: 'auditStatus',
title: '合同状态',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
},
},
{
field: 'operation',
title: '操作',
minWidth: 130,
align: 'center',
fixed: 'right',
cellRender: {
attrs: {
nameField: 'no',
nameTitle: '合同编号',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'processDetail',
show: hasAccessByCodes(['crm:contract:update']),
},
],
},
},
];
}
/** 客户跟进列表的搜索表单 */
export function useCustomerFollowFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'followUpStatus',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: FOLLOWUP_STATUS,
},
defaultValue: false,
},
];
}
/** 待进入公海客户列表的搜索表单 */
export function useCustomerPutPoolFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'sceneType',
label: '归属',
component: 'Select',
componentProps: {
allowClear: true,
options: SCENE_TYPES,
},
defaultValue: 1,
},
];
}
/** 今日需联系客户列表的搜索表单 */
export function useCustomerTodayContactFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'contactStatus',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: CONTACT_STATUS,
},
defaultValue: 1,
},
{
fieldName: 'sceneType',
label: '归属',
component: 'Select',
componentProps: {
allowClear: true,
options: SCENE_TYPES,
},
defaultValue: 1,
},
];
}
/** 客户列表的字段 */
export function useCustomerColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '客户名称',
minWidth: 160,
slots: {
default: 'name',
},
},
{
field: 'source',
title: '客户来源',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_SOURCE },
},
},
{
field: 'mobile',
title: '手机',
minWidth: 120,
},
{
field: 'telephone',
title: '电话',
minWidth: 130,
},
{
field: 'email',
title: '邮箱',
minWidth: 180,
},
{
field: 'level',
title: '客户级别',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_LEVEL },
},
},
{
field: 'industryId',
title: '客户行业',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY },
},
},
{
field: 'contactNextTime',
title: '下次联系时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'lockStatus',
title: '锁定状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'dealStatus',
title: '成交状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'contactLastTime',
title: '最后跟进时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'contactLastContent',
title: '最后跟进记录',
minWidth: 200,
},
{
field: 'detailAddress',
title: '地址',
minWidth: 200,
},
{
field: 'poolDay',
title: '距离进入公海天数',
minWidth: 180,
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 100,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
];
}
/** 回款审核列表的搜索表单 */
export function useReceivableAuditFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'auditStatus',
label: '合同状态',
component: 'Select',
componentProps: {
allowClear: true,
options: AUDIT_STATUS,
},
defaultValue: 10,
},
];
}
/** 回款审核列表的字段 */
export function useReceivableAuditColumns<T = CrmReceivableApi.Receivable>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'no',
title: '回款编号',
minWidth: 180,
fixed: 'left',
slots: {
default: 'no',
},
},
{
field: 'customerName',
title: '客户名称',
minWidth: 120,
slots: {
default: 'customerName',
},
},
{
field: 'contractNo',
title: '合同编号',
minWidth: 180,
slots: {
default: 'contractNo',
},
},
{
field: 'returnTime',
title: '回款日期',
minWidth: 150,
formatter: 'formatDateTime',
},
{
field: 'price',
title: '回款金额(元)',
minWidth: 140,
formatter: 'formatAmount',
},
{
field: 'returnType',
title: '回款方式',
minWidth: 130,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
},
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'contract.totalPrice',
title: '合同金额(元)',
minWidth: 140,
formatter: 'formatAmount',
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 120,
},
{
field: 'ownerUserDeptName',
title: '所属部门',
minWidth: 100,
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 120,
},
{
field: 'auditStatus',
title: '回款状态',
minWidth: 120,
fixed: 'right',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
},
},
{
field: 'operation',
title: '操作',
width: 140,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '角色',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'processDetail',
text: '查看审批',
show: hasAccessByCodes(['crm:receivable:update']),
},
],
},
},
];
}
/** 回款计划提醒列表的搜索表单 */
export function useReceivablePlanRemindFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'remindType',
label: '合同状态',
component: 'Select',
componentProps: {
allowClear: true,
options: RECEIVABLE_REMIND_TYPE,
},
defaultValue: 1,
},
];
}
/** 回款计划提醒列表的字段 */
export function useReceivablePlanRemindColumns<T = CrmReceivableApi.Receivable>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'customerName',
title: '客户名称',
minWidth: 160,
fixed: 'left',
slots: {
default: 'customerName',
},
},
{
field: 'contractNo',
title: '合同编号',
minWidth: 200,
},
{
field: 'period',
title: '期数',
minWidth: 160,
slots: {
default: 'period',
},
},
{
field: 'price',
title: '计划回款金额(元)',
minWidth: 120,
formatter: 'formatAmount',
},
{
field: 'returnTime',
title: '计划回款日期',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'remindDays',
title: '提前几天提醒',
minWidth: 150,
},
{
field: 'remindTime',
title: '提醒日期',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'returnType',
title: '回款方式',
minWidth: 120,
fixed: 'right',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
},
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
field: 'ownerUserName',
title: '负责人',
minWidth: 100,
},
{
field: 'receivable.price',
title: '实际回款金额(元)',
minWidth: 160,
formatter: 'formatAmount',
},
{
field: 'receivable.returnTime',
title: '实际回款日期',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'updateTime',
title: '更新时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
{
field: 'operation',
title: '操作',
width: 140,
fixed: 'right',
align: 'center',
cellRender: {
attrs: {
nameField: 'customerName',
nameTitle: '客户名称',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'receivableForm',
text: '创建回款',
show: hasAccessByCodes(['crm:receivable:create']),
},
],
},
},
];
}

View File

@@ -1,34 +1,121 @@
<script lang="ts" setup>
import { computed, onActivated, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { Badge, Card, List } from 'ant-design-vue';
import * as ClueApi from '#/api/crm/clue';
import * as ContractApi from '#/api/crm/contract';
import * as CustomerApi from '#/api/crm/customer';
import * as ReceivableApi from '#/api/crm/receivable';
import * as ReceivablePlanApi from '#/api/crm/receivable/plan';
import { DocAlert } from '#/components/doc-alert';
</script>
import { useLeftSides } from './data';
import ClueFollowList from './modules/ClueFollowList.vue';
import ContractAuditList from './modules/ContractAuditList.vue';
import ContractRemindList from './modules/ContractRemindList.vue';
import CustomerFollowList from './modules/CustomerFollowList.vue';
import CustomerPutPoolRemindList from './modules/CustomerPutPoolRemindList.vue';
import CustomerTodayContactList from './modules/CustomerTodayContactList.vue';
import ReceivableAuditList from './modules/ReceivableAuditList.vue';
import ReceivablePlanRemindList from './modules/ReceivablePlanRemindList.vue';
defineOptions({ name: 'CrmBacklog' });
const leftMenu = ref('customerTodayContact');
const clueFollowCount = ref(0);
const customerFollowCount = ref(0);
const customerPutPoolRemindCount = ref(0);
const customerTodayContactCount = ref(0);
const contractAuditCount = ref(0);
const contractRemindCount = ref(0);
const receivableAuditCount = ref(0);
const receivablePlanRemindCount = ref(0);
const leftSides = useLeftSides(
customerTodayContactCount,
clueFollowCount,
customerFollowCount,
customerPutPoolRemindCount,
contractAuditCount,
contractRemindCount,
receivableAuditCount,
receivablePlanRemindCount,
);
const currentComponent = computed(() => {
const components = {
customerTodayContact: CustomerTodayContactList,
clueFollow: ClueFollowList,
contractAudit: ContractAuditList,
receivableAudit: ReceivableAuditList,
contractRemind: ContractRemindList,
customerFollow: CustomerFollowList,
customerPutPoolRemind: CustomerPutPoolRemindList,
receivablePlanRemind: ReceivablePlanRemindList,
} as const;
return components[leftMenu.value as keyof typeof components];
});
/** 侧边点击 */
function sideClick(item: { menu: string }) {
leftMenu.value = item.menu;
}
/** 获取数量 */
async function getCount() {
customerTodayContactCount.value =
await CustomerApi.getTodayContactCustomerCount();
customerPutPoolRemindCount.value =
await CustomerApi.getPutPoolRemindCustomerCount();
customerFollowCount.value = await CustomerApi.getFollowCustomerCount();
clueFollowCount.value = await ClueApi.getFollowClueCount();
contractAuditCount.value = await ContractApi.getAuditContractCount();
contractRemindCount.value = await ContractApi.getRemindContractCount();
receivableAuditCount.value = await ReceivableApi.getAuditReceivableCount();
receivablePlanRemindCount.value =
await ReceivablePlanApi.getReceivablePlanRemindCount();
}
/** 激活时 */
onActivated(async () => {
getCount();
});
/** 初始化 */
onMounted(async () => {
getCount();
});
</script>
<template>
<Page>
<DocAlert
title="【通用】跟进记录、待办事项"
url="https://doc.iocoder.cn/crm/follow-up/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/backlog/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/backlog/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【通用】跟进记录、待办事项"
url="https://doc.iocoder.cn/crm/follow-up/"
/>
</template>
<div class="flex h-full w-full">
<Card class="w-1/5">
<List item-layout="horizontal" :data-source="leftSides">
<template #renderItem="{ item }">
<List.Item>
<List.Item.Meta>
<template #title>
<a @click="sideClick(item)"> {{ item.name }} </a>
</template>
</List.Item.Meta>
<template #extra v-if="item.count.value && item.count.value > 0">
<Badge :count="item.count.value" />
</template>
</List.Item>
</template>
</List>
</Card>
<component class="ml-4 w-4/5" :is="currentComponent" />
</div>
</Page>
</template>

View File

@@ -0,0 +1,58 @@
<!-- 分配给我的线索 -->
<script lang="ts" setup>
import type { CrmClueApi } from '#/api/crm/clue';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCluePage } from '#/api/crm/clue';
import { useClueFollowColumns, useClueFollowFormSchema } from '../data';
const { push } = useRouter();
/** 打开线索详情 */
function onDetail(row: CrmClueApi.Clue) {
push({ name: 'CrmClueDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useClueFollowFormSchema(),
},
gridOptions: {
columns: useClueFollowColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCluePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
transformStatus: false,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="分配给我的线索">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,111 @@
<!-- 待审核合同 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getContractPage } from '#/api/crm/contract';
import { useContractAuditFormSchema, useContractColumns } from '../data';
const { push } = useRouter();
/** 查看审批 */
function openProcessDetail(row: CrmContractApi.Contract) {
push({
name: 'BpmProcessInstanceDetail',
query: { id: row.processInstanceId },
});
}
/** 打开合同详情 */
function openContractDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContractDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
/** 打开联系人详情 */
function openContactDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContactDetail', params: { id: row.id } });
}
/** 打开商机详情 */
function openBusinessDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmContractApi.Contract>) {
switch (code) {
case 'processDetail': {
openProcessDetail(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useContractAuditFormSchema(),
},
gridOptions: {
columns: useContractColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getContractPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: 1, // 我负责的
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="待审核合同">
<template #name="{ row }">
<Button type="link" @click="openContractDetail(row)">
{{ row.name }}
</Button>
</template>
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #businessName="{ row }">
<Button type="link" @click="openBusinessDetail(row)">
{{ row.businessName }}
</Button>
</template>
<template #contactName="{ row }">
<Button type="link" @click="openContactDetail(row)">
{{ row.contactName }}
</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,111 @@
<!-- 即将到期的合同 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmContractApi } from '#/api/crm/contract';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getContractPage } from '#/api/crm/contract';
import { useContractColumns, useContractRemindFormSchema } from '../data';
const { push } = useRouter();
/** 查看审批 */
function openProcessDetail(row: CrmContractApi.Contract) {
push({
name: 'BpmProcessInstanceDetail',
query: { id: row.processInstanceId },
});
}
/** 打开合同详情 */
function openContractDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContractDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
/** 打开联系人详情 */
function openContactDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmContactDetail', params: { id: row.id } });
}
/** 打开商机详情 */
function openBusinessDetail(row: CrmContractApi.Contract) {
push({ name: 'CrmBusinessDetail', params: { id: row.id } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmContractApi.Contract>) {
switch (code) {
case 'processDetail': {
openProcessDetail(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useContractRemindFormSchema(),
},
gridOptions: {
columns: useContractColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getContractPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: 1, // 自己负责的
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="即将到期的合同">
<template #name="{ row }">
<Button type="link" @click="openContractDetail(row)">
{{ row.name }}
</Button>
</template>
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #businessName="{ row }">
<Button type="link" @click="openBusinessDetail(row)">
{{ row.businessName }}
</Button>
</template>
<template #contactName="{ row }">
<Button type="link" @click="openContactDetail(row)">
{{ row.contactName }}
</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,58 @@
<!-- 分配给我的客户 -->
<script lang="ts" setup>
import type { CrmCustomerApi } from '#/api/crm/customer';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCustomerPage } from '#/api/crm/customer';
import { useCustomerColumns, useCustomerFollowFormSchema } from '../data';
const { push } = useRouter();
/** 打开客户详情 */
function onDetail(row: CrmCustomerApi.Customer) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCustomerFollowFormSchema(),
},
gridOptions: {
columns: useCustomerColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCustomerPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: 1,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="分配给我的客户">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,58 @@
<!-- 待进入公海的客户 -->
<script lang="ts" setup>
import type { CrmCustomerApi } from '#/api/crm/customer';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCustomerPage } from '#/api/crm/customer';
import { useCustomerColumns, useCustomerPutPoolFormSchema } from '../data';
const { push } = useRouter();
/** 打开客户详情 */
function onDetail(row: CrmCustomerApi.Customer) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCustomerPutPoolFormSchema(),
},
gridOptions: {
columns: useCustomerColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCustomerPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
pool: true, // 固定 公海参数为 true
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="待进入公海的客户">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,58 @@
<!-- 今日需联系客户 -->
<script lang="ts" setup>
import type { CrmCustomerApi } from '#/api/crm/customer';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCustomerPage } from '#/api/crm/customer';
import { useCustomerColumns, useCustomerTodayContactFormSchema } from '../data';
const { push } = useRouter();
/** 打开客户详情 */
function onDetail(row: CrmCustomerApi.Customer) {
push({ name: 'CrmCustomerDetail', params: { id: row.id } });
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useCustomerTodayContactFormSchema(),
},
gridOptions: {
columns: useCustomerColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCustomerPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
pool: null, // 是否公海数据
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="今日需联系客户">
<template #name="{ row }">
<Button type="link" @click="onDetail(row)">{{ row.name }}</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,104 @@
<!-- 待审核回款 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmReceivableApi } from '#/api/crm/receivable';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getReceivablePage } from '#/api/crm/receivable';
import {
useReceivableAuditColumns,
useReceivableAuditFormSchema,
} from '../data';
const { push } = useRouter();
/** 查看审批 */
function openProcessDetail(row: CrmReceivableApi.Receivable) {
push({
name: 'BpmProcessInstanceDetail',
query: { id: row.processInstanceId },
});
}
/** 打开回款详情 */
function openDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 打开合同详情 */
function openContractDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmReceivableApi.Receivable>) {
switch (code) {
case 'processDetail': {
openProcessDetail(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useReceivableAuditFormSchema(),
},
gridOptions: {
columns: useReceivableAuditColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getReceivablePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="待审核回款">
<template #no="{ row }">
<Button type="link" @click="openDetail(row)">
{{ row.no }}
</Button>
</template>
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #contractNo="{ row }">
<Button type="link" @click="openContractDetail(row)">
{{ row.contractNo }}
</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,90 @@
<!-- 待回款提醒 -->
<script lang="ts" setup>
import type { OnActionClickParams } from '#/adapter/vxe-table';
import type { CrmReceivableApi } from '#/api/crm/receivable';
import { useRouter } from 'vue-router';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getReceivablePage } from '#/api/crm/receivable';
import {
useReceivablePlanRemindColumns,
useReceivablePlanRemindFormSchema,
} from '../data';
const { push } = useRouter();
/** 打开回款详情 */
function openDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmReceivableDetail', params: { id: row.id } });
}
/** 打开客户详情 */
function openCustomerDetail(row: CrmReceivableApi.Receivable) {
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 创建回款 */
function openReceivableForm(row: CrmReceivableApi.Receivable) {
// Todo: 打开创建回款
push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmReceivableApi.Receivable>) {
switch (code) {
case 'receivableForm': {
openReceivableForm(row);
break;
}
}
}
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useReceivablePlanRemindFormSchema(),
},
gridOptions: {
columns: useReceivablePlanRemindColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getReceivablePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
},
});
</script>
<template>
<Grid table-title="待回款提醒">
<template #customerName="{ row }">
<Button type="link" @click="openCustomerDetail(row)">
{{ row.customerName }}
</Button>
</template>
<template #period="{ row }">
<Button type="link" @click="openDetail(row)">{{ row.period }}</Button>
</template>
</Grid>
</template>

View File

@@ -0,0 +1,131 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
import { useAccess } from '@vben/access';
import { getRangePickerDefaultProps } from '@vben/utils';
import { z } from '#/adapter/form';
import { CommonStatusEnum } from '#/utils/constants';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'name',
label: '状态组名',
component: 'Input',
rules: 'required',
},
{
fieldName: 'deptIds',
label: '应用部门',
component: 'TreeSelect',
componentProps: {
multiple: true,
treeCheckable: true,
showCheckedStrategy: 'SHOW_PARENT',
placeholder: '请选择应用部门',
},
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '状态组名',
component: 'Input',
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns<T = CrmBusinessStatusApi.BusinessStatus>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'name',
title: '状态组名',
minWidth: 200,
},
{
field: 'deptNames',
title: '应用部门',
minWidth: 200,
formatter: ({ cellValue }) => {
return cellValue?.length > 0 ? cellValue.join(' ') : '全公司';
},
},
{
field: 'creator',
title: '创建人',
minWidth: 100,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'operation',
title: '操作',
width: 160,
fixed: 'right',
align: 'center',
cellRender: {
name: 'TableAction',
props: {
actions: [
{
label: '编辑',
code: 'edit',
show: hasAccessByCodes(['crm:business-status:update']),
},
{
label: '删除',
code: 'delete',
show: hasAccessByCodes(['crm:business-status:delete']),
},
],
onActionClick,
},
},
},
];
}

View File

@@ -1,38 +1,137 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteBusinessStatus,
getBusinessStatusPage,
} from '#/api/crm/business/status';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建商机状态 */
function onCreate() {
formModalApi.setData(null).open();
}
/** 删除商机状态 */
async function onDelete(row: CrmBusinessStatusApi.BusinessStatus) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
try {
await deleteBusinessStatus(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 编辑商机状态 */
function onEdit(row: CrmBusinessStatusApi.BusinessStatus) {
formModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<CrmBusinessStatusApi.BusinessStatus>) {
switch (code) {
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getBusinessStatusPage({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<CrmBusinessStatusApi.BusinessStatus>,
});
</script>
<template>
<Page>
<DocAlert
title="【商机】商机管理、商机状态"
url="https://doc.iocoder.cn/crm/business/"
/>
<DocAlert
title="【通用】数据权限"
url="https://doc.iocoder.cn/crm/permission/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/business/status/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/crm/business/status/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【商机】商机管理、商机状态"
url="https://doc.iocoder.cn/crm/business/"
/>
<DocAlert
title="【通用】数据权限"
url="https://doc.iocoder.cn/crm/permission/"
/>
</template>
<FormModal @success="onRefresh" />
<Grid table-title="商机状态列表">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="['crm:business-status:create']"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['商机状态']) }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createBusinessStatus,
getBusinessStatus,
updateBusinessStatus,
} from '#/api/crm/business/status';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<CrmBusinessStatusApi.BusinessStatusType>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['商机状态'])
: $t('ui.actionTitle.create', ['商机状态']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as CrmBusinessStatusApi.BusinessStatusType;
try {
await (formData.value?.id
? updateBusinessStatus(data)
: createBusinessStatus(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<CrmBusinessStatusApi.BusinessStatusType>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getBusinessStatus(data.id as number);
// 设置到 values
if (formData.value) {
await formApi.setValues(formData.value);
}
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-1/2">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -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>

View File

@@ -28,7 +28,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -28,7 +28,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -210,14 +210,14 @@ initDataSourceConfig();
<DocAlert
title="代码生成(单表)"
url="https://doc.iocoder.cn/new-feature/"
/>
<DocAlert
title="代码生成(树表)"
url="https://doc.iocoder.cn/new-feature/tree/"
/>
<DocAlert
title="代码生成(主子表)"
url="https://doc.iocoder.cn/new-feature/master-sub/"
/>
<DocAlert
title="代码生成(树表)"
url="https://doc.iocoder.cn/new-feature/tree/"
/>
<DocAlert
title="代码生成(主子表)"
url="https://doc.iocoder.cn/new-feature/master-sub/"
/>
<DocAlert title="单元测试" url="https://doc.iocoder.cn/unit-test/" />
</template>

View File

@@ -1,9 +1,10 @@
<script lang="ts" setup>
import type { InfraCodegenApi } from '#/api/infra/codegen';
import { useVbenForm } from '#/adapter/form';
import { watch } from 'vue';
import { useVbenForm } from '#/adapter/form';
import { useBasicInfoFormSchema } from '../data';
const props = defineProps<{

View File

@@ -2,11 +2,12 @@
import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { SystemDictTypeApi } from '#/api/system/dict/type';
import { nextTick, onMounted, ref, watch } from 'vue';
import { Checkbox, Input, Select } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleDictTypeList } from '#/api/system/dict/type';
import { nextTick, onMounted, ref, watch } from 'vue';
import { useCodegenColumnTableColumns } from '../data';

View File

@@ -110,7 +110,7 @@ const [Modal, modalApi] = useVbenModal({
});
} finally {
hideLoading();
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -22,6 +22,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -46,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -65,7 +72,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -53,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -72,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
import { useAccess } from '@vben/access';

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -52,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -71,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo01Contact(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
import { useAccess } from '@vben/access';
@@ -36,7 +34,6 @@ export function useFormSchema(): VbenFormSchema[] {
});
return handleTree(data);
},
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',

View File

@@ -31,6 +31,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -58,7 +65,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -79,7 +86,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo02Category(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
import { useAccess } from '@vben/access';

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useDemo03CourseFormSchema(),
showDefaultActions: false,
@@ -54,7 +61,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -73,7 +80,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo03Course(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useDemo03GradeFormSchema(),
showDefaultActions: false,
@@ -54,7 +61,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -73,7 +80,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo03Grade(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -52,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -71,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo03Student(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner';
import { useAccess } from '@vben/access';

View File

@@ -33,6 +33,13 @@ const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -54,9 +61,8 @@ const [Modal, modalApi] = useVbenModal({
// 提交表单
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
// 拼接子表的数据
// TODO @puhui999字段对不上
data.demo03Courses = demo03CourseFormRef.value?.getData();
data.demo03Grade = await demo03GradeFormRef.value?.getValues();
data.demo03courses = demo03CourseFormRef.value?.getData();
data.demo03grade = await demo03GradeFormRef.value?.getValues();
try {
await (formData.value?.id
? updateDemo03Student(data)
@@ -69,7 +75,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -88,7 +94,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo03Student(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
import { useAccess } from '@vben/access';

View File

@@ -33,6 +33,13 @@ const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -54,8 +61,8 @@ const [Modal, modalApi] = useVbenModal({
// 提交表单
const data = (await formApi.getValues()) as Demo03StudentApi.Demo03Student;
// 拼接子表的数据
data.demo03Courses = demo03CourseFormRef.value?.getData();
data.demo03Grade = await demo03GradeFormRef.value?.getValues();
data.demo03courses = demo03CourseFormRef.value?.getData();
data.demo03grade = await demo03GradeFormRef.value?.getValues();
try {
await (formData.value?.id
? updateDemo03Student(data)
@@ -68,7 +75,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -87,7 +94,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo03Student(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -80,7 +80,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -98,7 +98,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDemo01Contact(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
formData.value = data;

View File

@@ -14,12 +14,17 @@ import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
hideLabel: true,
},
layout: 'horizontal',
schema: useFormSchema().map((item) => ({ ...item, label: '' })), // 去除label
showDefaultActions: false,
commonConfig: {
hideLabel: true,
},
});
const [Modal, modalApi] = useVbenModal({
@@ -41,7 +46,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -36,7 +36,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE, 'number'),
placeholder: '请选择存储器',
class: 'w-full',
},
rules: 'required',
dependencies: {
@@ -87,7 +86,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
class: 'w-full',
controlsPosition: 'right',
placeholder: '请输入主机端口',
},

View File

@@ -27,7 +27,11 @@ const getTitle = computed(() => {
const [Form, formApi] = useVbenForm({
commonConfig: {
labelWidth: 120,
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
@@ -55,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -74,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -65,7 +65,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入重试次数。设置为 0 时,不进行重试',
min: 0,
class: 'w-full',
},
rules: 'required',
},
@@ -76,7 +75,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔',
min: 0,
class: 'w-full',
},
rules: 'required',
},
@@ -87,7 +85,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入监控超时时间,单位:毫秒',
min: 0,
class: 'w-full',
},
},
];

View File

@@ -29,7 +29,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = await getJobLog(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -32,7 +32,7 @@ const [Modal, modalApi] = useVbenModal({
// 获取下一次执行时间
nextTimes.value = await getJobNextTimes(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -22,12 +22,16 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
commonConfig: {
labelWidth: 140,
},
});
const [Modal, modalApi] = useVbenModal({
@@ -49,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -68,7 +72,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -0,0 +1,187 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import { useAccess } from '@vben/access';
import { getAppList } from '#/api/pay/app';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'appId',
label: '应用编号',
component: 'ApiSelect',
componentProps: {
api: async () => {
const data = await getAppList();
return data.map((item) => ({
label: item.name,
value: item.id,
}));
},
autoSelect: 'first',
placeholder: '请选择数据源',
},
},
{
fieldName: 'type',
label: '通知类型',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.PAY_NOTIFY_TYPE, 'number'),
},
},
{
fieldName: 'dataId',
label: '关联编号',
component: 'Input',
},
{
fieldName: 'status',
label: '通知状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.PAY_NOTIFY_STATUS, 'number'),
},
},
{
fieldName: 'merchantOrderId',
label: '商户订单编号',
component: 'Input',
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')],
},
},
];
}
/** 列表的字段 */
export function useGridColumns<T = any>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '任务编号',
minWidth: 100,
},
{
field: 'appName',
title: '应用编号',
minWidth: 120,
},
{
field: 'merchantOrderId',
title: '商户订单编号',
minWidth: 180,
},
{
field: 'type',
title: '通知类型',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_NOTIFY_TYPE },
},
},
{
field: 'dataId',
title: '关联编号',
minWidth: 120,
},
{
field: 'status',
title: '通知状态',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_NOTIFY_STATUS },
},
},
{
field: 'lastExecuteTime',
title: '最后通知时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'nextNotifyTime',
title: '下次通知时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'notifyTimes',
title: '通知次数',
minWidth: 120,
cellRender: {
name: 'CellTag',
props: {
type: 'success',
content: '{notifyTimes} / {maxNotifyTimes}',
},
},
},
{
field: 'operation',
title: '操作',
minWidth: 100,
align: 'center',
fixed: 'right',
cellRender: {
attrs: {
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
show: hasAccessByCodes(['pay:notify:query']),
},
],
},
},
];
}
/** 详情列表的字段 */
export const detailColumns = [
{
label: '日志编号',
prop: 'id',
key: 'id',
},
{
label: '通知状态',
prop: 'status',
key: 'status',
},
{
label: '通知次数',
prop: 'notifyTimes',
key: 'notifyTimes',
},
{
label: '通知时间',
prop: 'lastExecuteTime',
key: 'lastExecuteTime',
},
{
label: '响应结果',
prop: 'response',
key: 'response',
},
];

View File

@@ -1,31 +1,78 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import { Button } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import * as PayNotifyApi from '#/api/pay/notify';
import { DocAlert } from '#/components/doc-alert';
</script>
import { useGridColumns, useGridFormSchema } from './data';
import Detail from './modules/detail.vue';
const [NotifyDetailModal, notifyDetailModalApi] = useVbenModal({
connectedComponent: Detail,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 查看详情 */
function onDetail(row: any) {
notifyDetailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<any>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await PayNotifyApi.getNotifyTaskPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<any>,
});
</script>
<template>
<Page>
<DocAlert title="支付功能开启" url="https://doc.iocoder.cn/pay/build/" />
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/notify/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/notify/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert title="支付功能开启" url="https://doc.iocoder.cn/pay/build/" />
</template>
<NotifyDetailModal @success="onRefresh" />
<Grid table-title="支付通知列表" />
</Page>
</template>

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Descriptions, Divider, Table, Tag } from 'ant-design-vue';
import { getNotifyTaskDetail } from '#/api/pay/notify';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
import { detailColumns } from '../data';
const formData = ref();
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getNotifyTaskDetail(data.id);
} finally {
modalApi.unlock();
}
},
});
/** 打开弹窗 */
const open = (id: number) => {
modalApi.setData({ id }).open();
};
defineExpose({ open });
</script>
<template>
<Modal
title="通知详情"
class="w-1/2"
:show-cancel-button="false"
:show-confirm-button="false"
>
<Descriptions bordered :column="2" size="middle" class="mx-4">
<Descriptions.Item label="商户订单编号">
<Tag>{{ formData?.merchantOrderId }}</Tag>
</Descriptions.Item>
<Descriptions.Item label="通知状态">
<DictTag
:type="DICT_TYPE.PAY_NOTIFY_STATUS"
:value="formData?.status"
/>
</Descriptions.Item>
<Descriptions.Item label="应用编号">
{{ formData?.appId }}
</Descriptions.Item>
<Descriptions.Item label="应用名称">
{{ formData?.appName }}
</Descriptions.Item>
<Descriptions.Item label="关联编号">
{{ formData?.dataId }}
</Descriptions.Item>
<Descriptions.Item label="通知类型">
<DictTag :type="DICT_TYPE.PAY_NOTIFY_TYPE" :value="formData?.type" />
</Descriptions.Item>
<Descriptions.Item label="通知次数">
{{ formData?.notifyTimes }}
</Descriptions.Item>
<Descriptions.Item label="最大通知次数">
{{ formData?.maxNotifyTimes }}
</Descriptions.Item>
<Descriptions.Item label="最后通知时间">
{{ formatDateTime(formData?.lastExecuteTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="下次通知时间">
{{ formatDateTime(formData?.nextNotifyTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{{ formatDateTime(formData?.createTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{{ formatDateTime(formData?.updateTime || '') }}
</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions bordered :column="1" size="middle" class="mx-4">
<Descriptions.Item label="回调日志">
<Table :data="formData.logs" :columns="detailColumns" />
</Descriptions.Item>
</Descriptions>
</Modal>
</template>

View File

@@ -0,0 +1,177 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { PayRefundApi } from '#/api/pay/refund';
import { useAccess } from '@vben/access';
import { getAppList } from '#/api/pay/app';
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '#/utils/dict';
const { hasAccessByCodes } = useAccess();
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'appId',
label: '应用编号',
component: 'ApiSelect',
componentProps: {
api: async () => {
const data = await getAppList();
return data.map((item) => ({
label: item.name,
value: item.id,
}));
},
autoSelect: 'first',
placeholder: '请选择数据源',
},
},
{
fieldName: 'channelCode',
label: '退款渠道',
component: 'Select',
componentProps: {
allowClear: true,
options: getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE),
},
},
{
fieldName: 'merchantOrderId',
label: '商户支付单号',
component: 'Input',
},
{
fieldName: 'merchantRefundId',
label: '商户退款单号',
component: 'Input',
},
{
fieldName: 'channelOrderNo',
label: '渠道支付单号',
component: 'Input',
},
{
fieldName: 'channelRefundNo',
label: '渠道退款单号',
component: 'Input',
},
{
fieldName: 'status',
label: '退款状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getIntDictOptions(DICT_TYPE.PAY_REFUND_STATUS),
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'DatePicker',
componentProps: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')],
},
},
];
}
/** 列表的字段 */
export function useGridColumns<T = PayRefundApi.Refund>(
onActionClick: OnActionClickFn<T>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '编号',
minWidth: 100,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'payPrice',
title: '支付金额',
minWidth: 120,
cellRender: {
name: 'CellTag',
props: {
type: 'success',
content: '¥{payPrice}',
formatter: (value: number) => (value / 100).toFixed(2),
},
},
},
{
field: 'refundPrice',
title: '退款金额',
minWidth: 120,
cellRender: {
name: 'CellTag',
props: {
type: 'danger',
content: '¥{refundPrice}',
formatter: (value: number) => (value / 100).toFixed(2),
},
},
},
{
field: 'merchantRefundId',
title: '退款订单号',
minWidth: 300,
cellRender: {
name: 'CellTag',
props: {
type: 'info',
content: '商户 {merchantRefundId}',
},
},
},
{
field: 'channelRefundNo',
title: '渠道退款单号',
minWidth: 200,
cellRender: {
name: 'CellTag',
props: {
type: 'success',
content: '{channelRefundNo}',
},
},
},
{
field: 'status',
title: '退款状态',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.PAY_REFUND_STATUS },
},
},
{
field: 'operation',
title: '操作',
minWidth: 100,
align: 'center',
fixed: 'right',
cellRender: {
attrs: {
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'detail',
show: hasAccessByCodes(['pay:refund:query']),
},
],
},
},
];
}

View File

@@ -1,34 +1,104 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import { Page, useVbenModal } from '@vben/common-ui';
import { Download } from '@vben/icons';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import * as RefundApi from '#/api/pay/refund';
import { DocAlert } from '#/components/doc-alert';
</script>
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Detail from './modules/detail.vue';
const [RefundDetailModal, refundDetailModalApi] = useVbenModal({
connectedComponent: Detail,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出表格 */
async function onExport() {
const data = await RefundApi.exportRefund(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '支付退款.xls', source: data });
}
/** 查看详情 */
function onDetail(row: any) {
refundDetailModalApi.setData(row).open();
}
/** 表格操作按钮的回调函数 */
function onActionClick({ code, row }: OnActionClickParams<any>) {
switch (code) {
case 'detail': {
onDetail(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await RefundApi.getRefundPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<any>,
});
</script>
<template>
<Page>
<DocAlert
title="支付宝、微信退款接入"
url="https://doc.iocoder.cn/pay/refund-demo/"
/>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/refund/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/pay/refund/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert
title="支付宝、微信退款接入"
url="https://doc.iocoder.cn/pay/refund-demo/"
/>
</template>
<RefundDetailModal @success="onRefresh" />
<Grid table-title="支付退款列表">
<template #toolbar-tools>
<Button
type="primary"
class="ml-2"
@click="onExport"
v-access:code="['pay:refund:export']"
>
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,137 @@
<script lang="ts" setup>
import type { PayRefundApi } from '#/api/pay/refund';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Descriptions, Divider, Tag } from 'ant-design-vue';
import { getRefund } from '#/api/pay/refund';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } from '#/utils/dict';
const formData = ref<PayRefundApi.Refund>();
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<PayRefundApi.Refund>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getRefund(data.id);
} finally {
modalApi.unlock();
}
},
});
/** 打开弹窗 */
const open = (id: number) => {
modalApi.setData({ id }).open();
};
defineExpose({ open });
</script>
<template>
<Modal
title="退款详情"
class="w-1/2"
:show-cancel-button="false"
:show-confirm-button="false"
>
<Descriptions bordered :column="2" size="middle" class="mx-4">
<Descriptions.Item label="商户退款单号">
<Tag size="small">{{ formData?.merchantRefundId }}</Tag>
</Descriptions.Item>
<Descriptions.Item label="渠道退款单号">
<Tag type="success" size="small" v-if="formData?.channelRefundNo">
{{ formData?.channelRefundNo }}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="商户支付单号">
<Tag size="small">{{ formData?.merchantOrderId }}</Tag>
</Descriptions.Item>
<Descriptions.Item label="渠道支付单号">
<Tag type="success" size="small">
{{ formData?.channelOrderNo }}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="应用编号">
{{ formData?.appId }}
</Descriptions.Item>
<Descriptions.Item label="应用名称">
{{ formData?.appName }}
</Descriptions.Item>
<Descriptions.Item label="支付金额">
<Tag type="success" size="small">
{{ (formData?.payPrice || 0) / 100.0 }}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="退款金额">
<Tag size="mini" type="danger">
{{ (formData?.refundPrice || 0) / 100.0 }}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="退款状态">
<DictTag
:type="DICT_TYPE.PAY_REFUND_STATUS"
:value="formData?.status"
/>
</Descriptions.Item>
<Descriptions.Item label="退款时间">
{{ formatDateTime(formData?.successTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{{ formatDateTime(formData?.createTime || '') }}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{{ formatDateTime(formData?.updateTime || '') }}
</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions bordered :column="2" size="middle" class="mx-4">
<Descriptions.Item label="退款渠道">
<DictTag
:type="DICT_TYPE.PAY_CHANNEL_CODE"
:value="formData?.channelCode"
/>
</Descriptions.Item>
<Descriptions.Item label="退款原因">
{{ formData?.reason }}
</Descriptions.Item>
<Descriptions.Item label="退款 IP">
{{ formData?.userIp }}
</Descriptions.Item>
<Descriptions.Item label="通知 URL">
{{ formData?.notifyUrl }}
</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions bordered :column="2" size="middle" class="mx-4">
<Descriptions.Item label="渠道错误码">
{{ formData?.channelErrorCode }}
</Descriptions.Item>
<Descriptions.Item label="渠道错误码描述">
{{ formData?.channelErrorMsg }}
</Descriptions.Item>
</Descriptions>
<Descriptions bordered :column="1" size="middle" class="mx-4">
<Descriptions.Item label="支付通道异步回调内容">
<p class="whitespace-pre-wrap break-words">
{{ formData?.channelNotifyData }}
</p>
</Descriptions.Item>
</Descriptions>
</Modal>
</template>

View File

@@ -1,31 +1,30 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { ref } from 'vue';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import { DocAlert } from '#/components/doc-alert';
import { IFrame } from '#/components/iframe';
defineOptions({ name: 'GoView' });
const accessStore = useAccessStore();
const src = ref(
`${import.meta.env.VITE_GOVIEW_URL}?accessToken=${accessStore.accessToken}&refreshToken=${accessStore.refreshToken}`,
);
</script>
<template>
<Page>
<DocAlert title="大屏设计器" url="https://doc.iocoder.cn/report/screen/" />
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/report/goview/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/report/goview/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert
title="大屏设计器"
url="https://doc.iocoder.cn/report/screen/"
/>
</template>
<IFrame :src="src" />
</Page>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import { DocAlert } from '#/components/doc-alert';
import { IFrame } from '#/components/iframe';
defineOptions({ name: 'JimuBI' });
const accessStore = useAccessStore();
const src = ref(
`${import.meta.env.VITE_BASE_URL}/drag/list?token=${
accessStore.refreshToken
}`,
);
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="大屏设计器" url="https://doc.iocoder.cn/screen/" />
</template>
<IFrame :src="src" />
</Page>
</template>

View File

@@ -1,31 +1,28 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { ref } from 'vue';
import { Button } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import { DocAlert } from '#/components/doc-alert';
import { IFrame } from '#/components/iframe';
defineOptions({ name: 'JimuReport' });
const accessStore = useAccessStore();
const src = ref(
`${import.meta.env.VITE_BASE_URL}/jmreport/list?token=${
accessStore.refreshToken
}`,
);
</script>
<template>
<Page>
<DocAlert title="报表设计器" url="https://doc.iocoder.cn/report/" />
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/report/jmreport/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/report/jmreport/index
代码pull request 贡献给我们
</Button>
<Page auto-content-height>
<template #doc>
<DocAlert title="报表设计器" url="https://doc.iocoder.cn/report/" />
</template>
<IFrame :src="src" />
</Page>
</template>

View File

@@ -1,6 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemAreaApi } from '#/api/system/area';
/** 查询 IP 的表单 */

View File

@@ -10,6 +10,13 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data';
const [Form, { setFieldValue, validate, getValues }] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -33,7 +40,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -1,7 +1,5 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemDeptApi } from '#/api/system/dept';
import { useAccess } from '@vben/access';
@@ -40,7 +38,6 @@ export function useFormSchema(): VbenFormSchema[] {
});
return handleTree(data);
},
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
@@ -64,7 +61,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
class: 'w-full',
controlsPosition: 'right',
placeholder: '请输入显示顺序',
},
@@ -76,7 +72,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
class: 'w-full',
labelField: 'nickname',
valueField: 'id',
placeholder: '请选择负责人',

View File

@@ -22,6 +22,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -46,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -64,7 +71,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getDept(data.id);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values

View File

@@ -38,13 +38,15 @@ export function useTypeFormSchema(): VbenFormSchema[] {
fieldName: 'type',
label: '字典类型',
component: 'Input',
componentProps: {
placeholder: '请输入字典类型',
componentProps: (values) => {
return {
placeholder: '请输入字典类型',
disabled: !!values.id,
};
},
rules: 'required',
dependencies: {
triggerFields: [''],
disabled: ({ values }) => values.id,
},
},
{
@@ -107,9 +109,8 @@ export function useTypeGridColumns<T = SystemDictTypeApi.DictType>(
{
field: 'name',
title: '字典名称',
minWidth: 180,
minWidth: 200,
},
// TODO @芋艿disable的
{
field: 'type',
title: '字典类型',
@@ -118,7 +119,7 @@ export function useTypeGridColumns<T = SystemDictTypeApi.DictType>(
{
field: 'status',
title: '状态',
minWidth: 180,
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
@@ -204,7 +205,6 @@ export function useDataFormSchema(): VbenFormSchema[] {
return {
api: getSimpleDictTypeList,
placeholder: '请输入字典类型',
class: 'w-full',
labelField: 'name',
valueField: 'type',
disabled: !!values.id,
@@ -239,7 +239,6 @@ export function useDataFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
placeholder: '请输入显示排序',
class: 'w-full',
},
rules: 'required',
},
@@ -262,7 +261,6 @@ export function useDataFormSchema(): VbenFormSchema[] {
componentProps: {
options: colorOptions,
placeholder: '请选择颜色类型',
class: 'w-full',
},
},
{

View File

@@ -28,6 +28,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useDataFormSchema(),
showDefaultActions: false,
@@ -52,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -75,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
await formApi.setValues(formData.value);
}
} finally {
modalApi.lock(false);
modalApi.unlock();
}
} else if (data && 'dictType' in data && data.dictType) {
// 新增时如果传入了dictType则需要设置

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useTypeFormSchema(),
showDefaultActions: false,
@@ -50,7 +57,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -71,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
await formApi.setValues(formData.value);
}
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -28,7 +28,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -26,12 +26,16 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
commonConfig: {
labelWidth: 140,
},
});
const [Modal, modalApi] = useVbenModal({
@@ -56,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -75,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -28,7 +28,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -47,7 +47,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: async () => await getSimpleMailAccountList(),
class: 'w-full',
labelField: 'mail',
valueField: 'id',
placeholder: '请选择邮箱账号',

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -53,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -72,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -16,6 +16,13 @@ const emit = defineEmits(['success']);
const formData = ref<SystemMailTemplateApi.MailTemplate>();
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
showDefaultActions: false,
});
@@ -54,7 +61,7 @@ const [Modal, modalApi] = useVbenModal({
} catch (error) {
console.error('发送邮件失败', error);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {

View File

@@ -44,7 +44,6 @@ export function useFormSchema(): VbenFormSchema[] {
} as SystemMenuApi.Menu);
return handleTree(data);
},
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
@@ -167,7 +166,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'AutoComplete',
componentProps: {
allowClear: true,
class: 'w-full',
filterOption(input: string, option: { value: string }) {
return option.value.toLowerCase().includes(input.toLowerCase());
},
@@ -203,7 +201,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
class: 'w-full',
controlsPosition: 'right',
placeholder: '请输入显示顺序',
},

View File

@@ -22,6 +22,13 @@ const getTitle = computed(() =>
);
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -46,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -64,7 +71,7 @@ const [Modal, modalApi] = useVbenModal({
try {
data = await getMenu(data.id as number);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
}
// 设置到 values
@@ -75,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal :title="getTitle">
<Modal class="w-[40%]" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -22,6 +22,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -46,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -67,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
await formApi.setValues(formData.value);
}
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -28,7 +28,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -78,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
formData.value = data;
descApi.setState({ data });
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -68,7 +68,6 @@ export function useFormSchema(): VbenFormSchema[] {
DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
'number',
),
class: 'w-full',
placeholder: '请选择模板类型',
},
rules: 'required',
@@ -201,7 +200,6 @@ export function useSendNotifyFormSchema(): VbenFormSchema[] {
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
class: 'w-full',
labelField: 'nickname',
valueField: 'id',
placeholder: '请选择接收人',

View File

@@ -26,6 +26,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -53,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -72,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -17,11 +17,15 @@ const emit = defineEmits(['success']);
const formData = ref<SystemNotifyTemplateApi.NotifyTemplate>();
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
showDefaultActions: false,
commonConfig: {
labelWidth: 120,
},
});
const [Modal, modalApi] = useVbenModal({
@@ -59,7 +63,7 @@ const [Modal, modalApi] = useVbenModal({
} catch (error) {
console.error('发送站内信失败', error);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {

View File

@@ -48,11 +48,10 @@ export function useFormSchema(): VbenFormSchema[] {
},
rules: 'required',
},
// TODO @芋艿:图片上传
{
fieldName: 'logo',
label: '应用图标',
component: 'UploadImage',
component: 'ImageUpload',
componentProps: {
limit: 1,
},
@@ -84,7 +83,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入访问令牌的有效期,单位:秒',
min: 0,
class: 'w-full',
controlsPosition: 'right',
},
rules: 'required',
@@ -96,7 +94,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入刷新令牌的有效期,单位:秒',
min: 0,
class: 'w-full',
controlsPosition: 'right',
},
rules: 'required',
@@ -109,7 +106,6 @@ export function useFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE),
mode: 'multiple',
placeholder: '请输入授权类型',
class: 'w-full',
},
rules: 'required',
},
@@ -120,7 +116,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入授权范围',
mode: 'tags',
class: 'w-full',
},
},
{
@@ -130,7 +125,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入自动授权范围',
mode: 'multiple',
class: 'w-full',
// TODO @芋艿:根据权限,自动授权范围
},
},
@@ -141,7 +135,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入可重定向的 URI 地址',
mode: 'tags',
class: 'w-full',
},
rules: 'required',
},
@@ -152,7 +145,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
placeholder: '请输入权限',
mode: 'tags',
class: 'w-full',
},
},
{
@@ -162,7 +154,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
mode: 'tags',
placeholder: '请输入资源',
class: 'w-full',
},
},
{

View File

@@ -26,12 +26,16 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
commonConfig: {
labelWidth: 140,
},
});
const [Modal, modalApi] = useVbenModal({
@@ -56,7 +60,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -75,7 +79,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -25,7 +25,7 @@ const [Modal, modalApi] = useVbenModal({
try {
formData.value = data;
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -53,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -72,7 +72,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -40,7 +40,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'InputNumber',
componentProps: {
min: 0,
class: 'w-full',
controlsPosition: 'right',
placeholder: '请输入显示顺序',
},
@@ -97,7 +96,6 @@ export function useAssignDataPermissionFormSchema(): VbenFormSchema[] {
fieldName: 'dataScope',
label: '权限范围',
componentProps: {
class: 'w-full',
options: getDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE, 'number'),
},
},

View File

@@ -64,7 +64,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -83,7 +83,7 @@ const [Modal, modalApi] = useVbenModal({
await loadDeptTree();
toggleExpandAll();
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -25,6 +25,13 @@ const isExpanded = ref(false); // 展开状态
const expandedKeys = ref<number[]>([]); // 展开的节点
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useAssignMenuFormSchema(),
showDefaultActions: false,
@@ -52,7 +59,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -73,7 +80,7 @@ const [Modal, modalApi] = useVbenModal({
// 加载菜单列表
await loadMenuTree();
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -22,6 +22,13 @@ const getTitle = computed(() => {
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
@@ -46,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
@@ -65,7 +72,7 @@ const [Modal, modalApi] = useVbenModal({
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
modalApi.unlock();
}
},
});

View File

@@ -37,7 +37,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, 'string'),
class: 'w-full',
placeholder: '请选择短信渠道',
},
rules: 'required',

Some files were not shown because too many files have changed in this diff Show More