chore: 保证和上游Vben-admin的框架依赖一致性
commit
615749daf1
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'@vben/styles': patch
|
||||||
|
'@vben-core/form-ui': patch
|
||||||
|
'@vben/web-naive': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray
|
||||||
|
|
@ -21,11 +21,6 @@ export async function createLeave(data: BpmOALeaveApi.Leave) {
|
||||||
return requestClient.post('/bpm/oa/leave/create', data);
|
return requestClient.post('/bpm/oa/leave/create', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新请假申请 */
|
|
||||||
export async function updateLeave(data: BpmOALeaveApi.Leave) {
|
|
||||||
return requestClient.post('/bpm/oa/leave/update', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获得请假申请 */
|
/** 获得请假申请 */
|
||||||
export async function getLeave(id: number) {
|
export async function getLeave(id: number) {
|
||||||
return requestClient.get<BpmOALeaveApi.Leave>(`/bpm/oa/leave/get?id=${id}`);
|
return requestClient.get<BpmOALeaveApi.Leave>(`/bpm/oa/leave/get?id=${id}`);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ export namespace CrmReceivablePlanApi {
|
||||||
returnTime: Date;
|
returnTime: Date;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export interface PlanPageParam extends PageParam {
|
||||||
|
customerId?: number;
|
||||||
|
contractId?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询回款计划列表 */
|
/** 查询回款计划列表 */
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
|
||||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||||
const strTimeDuration =
|
const strTimeDuration =
|
||||||
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
@ -359,12 +359,12 @@ const loadFormInfo = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIsoTimeDuration = () => {
|
const getIsoTimeDuration = () => {
|
||||||
let strTimeDuration = 'PT';
|
let strTimeDuration = 'P';
|
||||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
strTimeDuration += `T${configForm.value.timeDuration}M`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
strTimeDuration += `T${configForm.value.timeDuration}H`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,12 @@ function getShowText(): string {
|
||||||
|
|
||||||
// 获取ISO时间格式
|
// 获取ISO时间格式
|
||||||
function getIsoTimeDuration() {
|
function getIsoTimeDuration() {
|
||||||
let strTimeDuration = 'PT';
|
let strTimeDuration = 'P';
|
||||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
strTimeDuration += `T${configForm.value.timeDuration}M`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
strTimeDuration += `T${configForm.value.timeDuration}H`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||||
|
|
@ -135,7 +135,7 @@ function openDrawer(node: SimpleFlowNode) {
|
||||||
// 固定时长
|
// 固定时长
|
||||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||||
const strTimeDuration = node.delaySetting.delayTime;
|
const strTimeDuration = node.delaySetting.delayTime;
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,7 @@ function showUserTaskNodeConfig(node: SimpleFlowNode) {
|
||||||
configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable;
|
configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable;
|
||||||
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
|
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
|
||||||
const strTimeDuration = node.timeoutHandler.timeDuration;
|
const strTimeDuration = node.timeoutHandler.timeDuration;
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
timeUnit.value = convertTimeUnit(parseTimeUnit);
|
timeUnit.value = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
@ -563,12 +563,12 @@ function useTimeoutHandler() {
|
||||||
if (!configForm.value.timeoutHandlerEnable) {
|
if (!configForm.value.timeoutHandlerEnable) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let strTimeDuration = 'PT';
|
let strTimeDuration = 'P';
|
||||||
if (timeUnit.value === TimeUnitType.MINUTE) {
|
if (timeUnit.value === TimeUnitType.MINUTE) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
strTimeDuration += `T${configForm.value.timeDuration}M`;
|
||||||
}
|
}
|
||||||
if (timeUnit.value === TimeUnitType.HOUR) {
|
if (timeUnit.value === TimeUnitType.HOUR) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
strTimeDuration += `T${configForm.value.timeDuration}H`;
|
||||||
}
|
}
|
||||||
if (timeUnit.value === TimeUnitType.DAY) {
|
if (timeUnit.value === TimeUnitType.DAY) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Button, Card, Col, message, Row, Space } from 'ant-design-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { getProcessDefinition } from '#/api/bpm/definition';
|
import { getProcessDefinition } from '#/api/bpm/definition';
|
||||||
import { createLeave, getLeave, updateLeave } from '#/api/bpm/oa/leave';
|
import { createLeave, getLeave } from '#/api/bpm/oa/leave';
|
||||||
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
|
@ -88,9 +88,7 @@ async function onSubmit() {
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
formLoading.value = true;
|
formLoading.value = true;
|
||||||
await (formData.value?.id
|
await createLeave(submitData);
|
||||||
? updateLeave(submitData)
|
|
||||||
: createLeave(submitData));
|
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
await closeCurrentTab();
|
await closeCurrentTab();
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ function shouldShowCustomUserSelect(
|
||||||
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
||||||
return (
|
return (
|
||||||
task.reason &&
|
task.reason &&
|
||||||
[BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||||
nodeType,
|
nodeType,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||||
</WorkbenchHeader>
|
</WorkbenchHeader>
|
||||||
|
|
||||||
<div class="mt-5 flex flex-col lg:flex-row">
|
<div class="flex flex-col lg:flex-row">
|
||||||
<div class="mr-4 w-full lg:w-3/5">
|
<div class="mr-4 w-full lg:w-3/5">
|
||||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||||
|
|
@ -246,7 +246,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||||
<div class="w-full lg:w-2/5">
|
<div class="w-full lg:w-2/5">
|
||||||
<WorkbenchQuickNav
|
<WorkbenchQuickNav
|
||||||
:items="quickNavItems"
|
:items="quickNavItems"
|
||||||
class="mt-5 lg:mt-0"
|
class="lg:mt-0"
|
||||||
title="快捷导航"
|
title="快捷导航"
|
||||||
@click="navTo"
|
@click="navTo"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,12 @@ function handleColorChange(event: Event) {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.route-color-picker__swatch {
|
.route-color-picker__swatch {
|
||||||
|
inline-size: 36px;
|
||||||
block-size: 28px;
|
block-size: 28px;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
border: 1px solid var(--ant-color-border, #d9d9d9);
|
border: 1px solid var(--ant-color-border, #d9d9d9);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
|
||||||
inline-size: 36px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-color-picker__swatch:disabled {
|
.route-color-picker__swatch:disabled {
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,20 @@ function initGantt() {
|
||||||
|
|
||||||
gantt.config.columns = [
|
gantt.config.columns = [
|
||||||
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
|
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
|
||||||
{ name: 'workstation', label: '工作站', align: 'center', width: 100, resize: true },
|
{
|
||||||
{ name: 'process', label: '工序', align: 'center', width: 100, resize: true },
|
name: 'workstation',
|
||||||
|
label: '工作站',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
resize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'process',
|
||||||
|
label: '工序',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
resize: true,
|
||||||
|
},
|
||||||
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
|
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
|
||||||
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 },
|
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 },
|
||||||
];
|
];
|
||||||
|
|
@ -194,7 +206,10 @@ defineExpose({ loadData });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="ganttContainer" :style="{ width: '100%', height: `${height}px` }"></div>
|
<div
|
||||||
|
ref="ganttContainer"
|
||||||
|
:style="{ width: '100%', height: `${height}px` }"
|
||||||
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
||||||
|
|
||||||
import { DICT_TYPE, generateWmsCode, h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { CommonStatusEnum } from '@vben/constants';
|
import { CommonStatusEnum, DICT_TYPE, generateWmsCode } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { handleTree } from '@vben/utils';
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { generateWmsCode, h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, generateWmsCode } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,6 @@ export async function createLeave(data: BpmOALeaveApi.Leave) {
|
||||||
return requestClient.post('/bpm/oa/leave/create', data);
|
return requestClient.post('/bpm/oa/leave/create', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新请假申请 */
|
|
||||||
export async function updateLeave(data: BpmOALeaveApi.Leave) {
|
|
||||||
return requestClient.post('/bpm/oa/leave/update', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获得请假申请 */
|
/** 获得请假申请 */
|
||||||
export async function getLeave(id: number) {
|
export async function getLeave(id: number) {
|
||||||
return requestClient.get<BpmOALeaveApi.Leave>(`/bpm/oa/leave/get?id=${id}`);
|
return requestClient.get<BpmOALeaveApi.Leave>(`/bpm/oa/leave/get?id=${id}`);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ export namespace CrmReceivablePlanApi {
|
||||||
returnTime: Date;
|
returnTime: Date;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export interface PlanPageParam extends PageParam {
|
||||||
|
customerId?: number;
|
||||||
|
contractId?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询回款计划列表 */
|
/** 查询回款计划列表 */
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
|
||||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||||
const strTimeDuration =
|
const strTimeDuration =
|
||||||
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
@ -358,12 +358,12 @@ const loadFormInfo = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIsoTimeDuration = () => {
|
const getIsoTimeDuration = () => {
|
||||||
let strTimeDuration = 'PT';
|
let strTimeDuration = 'P';
|
||||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
strTimeDuration += `T${configForm.value.timeDuration}M`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
strTimeDuration += `T${configForm.value.timeDuration}H`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,12 @@ function getShowText(): string {
|
||||||
|
|
||||||
// 获取ISO时间格式
|
// 获取ISO时间格式
|
||||||
function getIsoTimeDuration() {
|
function getIsoTimeDuration() {
|
||||||
let strTimeDuration = 'PT';
|
let strTimeDuration = 'P';
|
||||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
strTimeDuration += `T${configForm.value.timeDuration}M`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
strTimeDuration += `T${configForm.value.timeDuration}H`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||||
|
|
@ -134,7 +134,7 @@ function openDrawer(node: SimpleFlowNode) {
|
||||||
// 固定时长
|
// 固定时长
|
||||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||||
const strTimeDuration = node.delaySetting.delayTime;
|
const strTimeDuration = node.delaySetting.delayTime;
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,7 @@ function showUserTaskNodeConfig(node: SimpleFlowNode) {
|
||||||
configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable;
|
configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable;
|
||||||
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
|
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
|
||||||
const strTimeDuration = node.timeoutHandler.timeDuration;
|
const strTimeDuration = node.timeoutHandler.timeDuration;
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
timeUnit.value = convertTimeUnit(parseTimeUnit);
|
timeUnit.value = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Button, Card, Col, message, Row, Space } from 'antdv-next';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { getProcessDefinition } from '#/api/bpm/definition';
|
import { getProcessDefinition } from '#/api/bpm/definition';
|
||||||
import { createLeave, getLeave, updateLeave } from '#/api/bpm/oa/leave';
|
import { createLeave, getLeave } from '#/api/bpm/oa/leave';
|
||||||
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
|
@ -88,9 +88,7 @@ async function onSubmit() {
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
formLoading.value = true;
|
formLoading.value = true;
|
||||||
await (formData.value?.id
|
await createLeave(submitData);
|
||||||
? updateLeave(submitData)
|
|
||||||
: createLeave(submitData));
|
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
await closeCurrentTab();
|
await closeCurrentTab();
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ function shouldShowCustomUserSelect(
|
||||||
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
||||||
return (
|
return (
|
||||||
task.reason &&
|
task.reason &&
|
||||||
[BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||||
nodeType,
|
nodeType,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,6 @@ export async function createLeave(data: BpmOALeaveApi.Leave) {
|
||||||
return requestClient.post('/bpm/oa/leave/create', data);
|
return requestClient.post('/bpm/oa/leave/create', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新请假申请 */
|
|
||||||
export async function updateLeave(data: BpmOALeaveApi.Leave) {
|
|
||||||
return requestClient.post('/bpm/oa/leave/update', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获得请假申请 */
|
/** 获得请假申请 */
|
||||||
export async function getLeave(id: number) {
|
export async function getLeave(id: number) {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ export namespace CrmReceivablePlanApi {
|
||||||
returnTime: Date;
|
returnTime: Date;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export interface PlanPageParam extends PageParam {
|
||||||
|
customerId?: number;
|
||||||
|
contractId?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询回款计划列表 */
|
/** 查询回款计划列表 */
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
|
||||||
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||||
const strTimeDuration =
|
const strTimeDuration =
|
||||||
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
node.childProcessSetting.timeoutSetting.timeExpression ?? '';
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
@ -346,12 +346,12 @@ const loadFormInfo = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIsoTimeDuration = () => {
|
const getIsoTimeDuration = () => {
|
||||||
let strTimeDuration = 'PT';
|
let strTimeDuration = 'P';
|
||||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
strTimeDuration += `T${configForm.value.timeDuration}M`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
strTimeDuration += `T${configForm.value.timeDuration}H`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,12 @@ function getShowText(): string {
|
||||||
|
|
||||||
// 获取ISO时间格式
|
// 获取ISO时间格式
|
||||||
function getIsoTimeDuration() {
|
function getIsoTimeDuration() {
|
||||||
let strTimeDuration = 'PT';
|
let strTimeDuration = 'P';
|
||||||
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}M`;
|
strTimeDuration += `T${configForm.value.timeDuration}M`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}H`;
|
strTimeDuration += `T${configForm.value.timeDuration}H`;
|
||||||
}
|
}
|
||||||
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
if (configForm.value.timeUnit === TimeUnitType.DAY) {
|
||||||
strTimeDuration += `${configForm.value.timeDuration}D`;
|
strTimeDuration += `${configForm.value.timeDuration}D`;
|
||||||
|
|
@ -134,7 +134,7 @@ function openDrawer(node: SimpleFlowNode) {
|
||||||
// 固定时长
|
// 固定时长
|
||||||
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
|
||||||
const strTimeDuration = node.delaySetting.delayTime;
|
const strTimeDuration = node.delaySetting.delayTime;
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,7 @@ function showUserTaskNodeConfig(node: SimpleFlowNode) {
|
||||||
configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable;
|
configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable;
|
||||||
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
|
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
|
||||||
const strTimeDuration = node.timeoutHandler.timeDuration;
|
const strTimeDuration = node.timeoutHandler.timeDuration;
|
||||||
const parseTime = strTimeDuration.slice(2, -1);
|
const parseTime = strTimeDuration.match(/\d+/)?.[0] ?? '';
|
||||||
const parseTimeUnit = strTimeDuration.slice(-1);
|
const parseTimeUnit = strTimeDuration.slice(-1);
|
||||||
configForm.value.timeDuration = Number.parseInt(parseTime);
|
configForm.value.timeDuration = Number.parseInt(parseTime);
|
||||||
timeUnit.value = convertTimeUnit(parseTimeUnit);
|
timeUnit.value = convertTimeUnit(parseTimeUnit);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { getProcessDefinition } from '#/api/bpm/definition';
|
import { getProcessDefinition } from '#/api/bpm/definition';
|
||||||
import { createLeave, getLeave, updateLeave } from '#/api/bpm/oa/leave';
|
import { createLeave, getLeave } from '#/api/bpm/oa/leave';
|
||||||
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
|
|
@ -95,9 +95,7 @@ async function onSubmit() {
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
formLoading.value = true;
|
formLoading.value = true;
|
||||||
await (formData.value?.id
|
await createLeave(submitData);
|
||||||
? updateLeave(submitData)
|
|
||||||
: createLeave(submitData));
|
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||||
await closeCurrentTab();
|
await closeCurrentTab();
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import { registerComponent } from '#/utils';
|
||||||
|
|
||||||
import ProcessInstanceBpmnViewer from './modules/bpm-viewer.vue';
|
import ProcessInstanceBpmnViewer from './modules/bpm-viewer.vue';
|
||||||
import ProcessInstanceOperationButton from './modules/operation-button.vue';
|
import ProcessInstanceOperationButton from './modules/operation-button.vue';
|
||||||
import ProcessssPrint from './modules/process-print.vue';
|
import ProcessPrint from './modules/process-print.vue';
|
||||||
import ProcessInstanceSimpleViewer from './modules/simple-bpm-viewer.vue';
|
import ProcessInstanceSimpleViewer from './modules/simple-bpm-viewer.vue';
|
||||||
import BpmProcessInstanceTaskList from './modules/task-list.vue';
|
import BpmProcessInstanceTaskList from './modules/task-list.vue';
|
||||||
import ProcessInstanceTimeline from './modules/time-line.vue';
|
import ProcessInstanceTimeline from './modules/time-line.vue';
|
||||||
|
|
@ -200,7 +200,7 @@ const refresh = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const [PrintModal, printModalApi] = useVbenModal({
|
const [PrintModal, printModalApi] = useVbenModal({
|
||||||
connectedComponent: ProcessssPrint,
|
connectedComponent: ProcessPrint,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -343,7 +343,12 @@ onMounted(async () => {
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
<ElTabPane label="流程图" name="diagram" class="pb-20 pr-3">
|
<ElTabPane
|
||||||
|
label="流程图"
|
||||||
|
name="diagram"
|
||||||
|
:lazy="false"
|
||||||
|
class="pb-20 pr-3"
|
||||||
|
>
|
||||||
<ProcessInstanceSimpleViewer
|
<ProcessInstanceSimpleViewer
|
||||||
v-show="
|
v-show="
|
||||||
processDefinition.modelType &&
|
processDefinition.modelType &&
|
||||||
|
|
@ -420,7 +425,21 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-tabs__content) {
|
:deep(.el-tabs__content) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tab-pane) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 流程图 tab 特殊处理:需要内部 flex 布局 */
|
||||||
|
:deep(#pane-diagram) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ watch(
|
||||||
view.value = newModelView;
|
view.value = newModelView;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 监听 bpmnXml */
|
/** 监听 bpmnXml */
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ async function openPopover(type: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.keys(popOverVisible.value).forEach((item) => {
|
Object.keys(popOverVisible.value).forEach((item) => {
|
||||||
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
|
popOverVisible.value[item] = item === type;
|
||||||
});
|
});
|
||||||
if (type === 'approve') {
|
if (type === 'approve') {
|
||||||
// 当前任务有节点表单时,等 form-create 的 fApi 就绪后再计算下一个节点;
|
// 当前任务有节点表单时,等 form-create 的 fApi 就绪后再计算下一个节点;
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ watch(
|
||||||
simpleModel.value = newModelView.simpleModel || {};
|
simpleModel.value = newModelView.simpleModel || {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 监控模型结构数据 */
|
/** 监控模型结构数据 */
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ function shouldShowCustomUserSelect(
|
||||||
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
||||||
return (
|
return (
|
||||||
task.reason &&
|
task.reason &&
|
||||||
[BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||||
nodeType,
|
nodeType,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import {
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
|
||||||
// TODO @芋艿:风格和 antd 不一致;
|
// TODO @芋艿:风格和 antd 不一致;
|
||||||
const summary = ref<MallOrderApi.OrderSummary>();
|
const summary = ref<MallOrderApi.OrderSummaryRespVO>();
|
||||||
|
|
||||||
/** 刷新表格 */
|
/** 刷新表格 */
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,20 @@ function initGantt() {
|
||||||
|
|
||||||
gantt.config.columns = [
|
gantt.config.columns = [
|
||||||
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
|
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
|
||||||
{ name: 'workstation', label: '工作站', align: 'center', width: 100, resize: true },
|
{
|
||||||
{ name: 'process', label: '工序', align: 'center', width: 100, resize: true },
|
name: 'workstation',
|
||||||
|
label: '工作站',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
resize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'process',
|
||||||
|
label: '工序',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
resize: true,
|
||||||
|
},
|
||||||
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
|
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
|
||||||
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 },
|
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 },
|
||||||
];
|
];
|
||||||
|
|
@ -194,7 +206,10 @@ defineExpose({ loadData });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="ganttContainer" :style="{ width: '100%', height: `${height}px` }"></div>
|
<div
|
||||||
|
ref="ganttContainer"
|
||||||
|
:style="{ width: '100%', height: `${height}px` }"
|
||||||
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { WmsHomeStatisticsApi } from '#/api/wms/home';
|
import type { WmsHomeStatisticsApi } from '#/api/wms/home';
|
||||||
|
|
||||||
import { OrderStatusEnum, OrderTypeEnum, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, OrderStatusEnum, OrderTypeEnum } from '@vben/constants';
|
||||||
import { getDictLabel } from '@vben/hooks';
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
|
||||||
import { ElButton, ElCard, ElMessage, ElSkeleton } from 'element-plus';
|
import { ElButton, ElCard, ElMessage, ElSkeleton } from 'element-plus';
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
||||||
|
|
||||||
import { DICT_TYPE, generateWmsCode, h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { CommonStatusEnum } from '@vben/constants';
|
import { CommonStatusEnum, DICT_TYPE, generateWmsCode } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { handleTree } from '@vben/utils';
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { generateWmsCode, h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, generateWmsCode } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { ElButton } from 'element-plus';
|
import { ElButton } from 'element-plus';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { NButton, NCard, useMessage } from 'naive-ui';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
layout: 'vertical',
|
||||||
|
wrapperClass: 'grid-cols-1',
|
||||||
|
handleSubmit: (values) => {
|
||||||
|
message.success(`提交成功:${JSON.stringify(values)}`);
|
||||||
|
},
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'projectName',
|
||||||
|
label: '项目名称',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenFormFieldArray',
|
||||||
|
fieldName: 'members',
|
||||||
|
label: '项目成员',
|
||||||
|
// 初始化为空数组,供内部 useFieldArray 使用
|
||||||
|
defaultValue: [],
|
||||||
|
componentProps: {
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
createRow: () => ({
|
||||||
|
name: null,
|
||||||
|
age: null,
|
||||||
|
role: null,
|
||||||
|
joinDate: null,
|
||||||
|
active: true,
|
||||||
|
}),
|
||||||
|
// 每一列就是一个子字段,复用 vbenForm 的所有编辑组件
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '姓名',
|
||||||
|
rules: 'required',
|
||||||
|
componentProps: { placeholder: '请输入姓名' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'InputNumber',
|
||||||
|
fieldName: 'age',
|
||||||
|
label: '年龄',
|
||||||
|
componentProps: { min: 0, max: 150 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'role',
|
||||||
|
label: '角色',
|
||||||
|
rules: 'selectRequired',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择',
|
||||||
|
options: [
|
||||||
|
{ label: '前端', value: 'fe' },
|
||||||
|
{ label: '后端', value: 'be' },
|
||||||
|
{ label: '测试', value: 'qa' },
|
||||||
|
{ label: '产品', value: 'pm' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'DatePicker',
|
||||||
|
fieldName: 'joinDate',
|
||||||
|
label: '入职日期',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Switch',
|
||||||
|
fieldName: 'active',
|
||||||
|
label: '在职',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
function setFormValues() {
|
||||||
|
formApi.setValues({
|
||||||
|
projectName: 'Vben Admin',
|
||||||
|
members: [
|
||||||
|
{ name: '张三', age: 28, role: 'fe', joinDate: Date.now(), active: true },
|
||||||
|
{
|
||||||
|
name: '李四',
|
||||||
|
age: 32,
|
||||||
|
role: 'be',
|
||||||
|
joinDate: Date.now(),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFormValues() {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
message.info(JSON.stringify(values));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
description="基于 useVbenForm 的数组编辑器(VbenFormFieldArray):可增删行,每个单元格复用 vbenForm 注册的编辑组件,并享受逐格校验。"
|
||||||
|
title="数组编辑器表单"
|
||||||
|
>
|
||||||
|
<NCard title="数组编辑器">
|
||||||
|
<template #header-extra>
|
||||||
|
<NButton class="mr-2" @click="setFormValues">设置表单值</NButton>
|
||||||
|
<NButton class="mr-2" @click="getFormValues">获取表单值</NButton>
|
||||||
|
<NButton type="primary" @click="formApi.submitForm()">
|
||||||
|
提交校验
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
<Form />
|
||||||
|
</NCard>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -3,6 +3,7 @@ import type { GlobalConfigProvider } from 'tdesign-vue-next';
|
||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { useTDesignDesignTokens } from '@vben/hooks';
|
||||||
import { usePreferences } from '@vben/preferences';
|
import { usePreferences } from '@vben/preferences';
|
||||||
|
|
||||||
import { merge } from 'es-toolkit/compat';
|
import { merge } from 'es-toolkit/compat';
|
||||||
|
|
@ -12,10 +13,16 @@ import zhConfig from 'tdesign-vue-next/es/locale/zh_CN';
|
||||||
defineOptions({ name: 'App' });
|
defineOptions({ name: 'App' });
|
||||||
const { isDark } = usePreferences();
|
const { isDark } = usePreferences();
|
||||||
|
|
||||||
|
// 将 Vben 设计系统的 CSS 变量适配到 TDesign 的设计变量上
|
||||||
|
useTDesignDesignTokens();
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => isDark.value,
|
() => isDark.value,
|
||||||
(dark) => {
|
(dark) => {
|
||||||
document.documentElement.setAttribute('theme-mode', dark ? 'dark' : '');
|
document.documentElement.setAttribute(
|
||||||
|
'theme-mode',
|
||||||
|
dark ? 'dark' : 'light',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { initStores } from '@vben/stores';
|
import { initStores } from '@vben/stores';
|
||||||
import '@vben/styles';
|
import '@vben/styles';
|
||||||
// import '@vben/styles/antd';
|
|
||||||
// 引入组件库的少量全局样式变量
|
|
||||||
|
|
||||||
import { useTitle } from '@vueuse/core';
|
import { useTitle } from '@vueuse/core';
|
||||||
|
|
||||||
|
|
@ -17,6 +15,7 @@ import { initSetupVbenForm } from './adapter/form';
|
||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
|
|
||||||
|
// 引入组件库的少量全局样式变量
|
||||||
import 'tdesign-vue-next/es/style/index.css';
|
import 'tdesign-vue-next/es/style/index.css';
|
||||||
|
|
||||||
async function bootstrap(namespace: string) {
|
async function bootstrap(namespace: string) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { computed, ref, useSlots } from 'vue';
|
||||||
|
|
||||||
import { VbenTooltip } from '@vben-core/shadcn-ui';
|
import { VbenTooltip } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { Code } from 'lucide-vue-next';
|
import { Code } from '@lucide/vue';
|
||||||
import {
|
import {
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsIndicator,
|
TabsIndicator,
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||||
link: 'common-ui/vben-ellipsis-text',
|
link: 'common-ui/vben-ellipsis-text',
|
||||||
text: 'EllipsisText',
|
text: 'EllipsisText',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-descriptions',
|
||||||
|
text: 'Descriptions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-table-action',
|
||||||
|
text: 'TableAction',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
link: 'common-ui/vben-cropper',
|
link: 'common-ui/vben-cropper',
|
||||||
text: 'Cropper',
|
text: 'Cropper',
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||||
link: 'common-ui/vben-ellipsis-text',
|
link: 'common-ui/vben-ellipsis-text',
|
||||||
text: 'EllipsisText 省略文本',
|
text: 'EllipsisText 省略文本',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-descriptions',
|
||||||
|
text: 'Descriptions 描述列表',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-table-action',
|
||||||
|
text: 'TableAction 表格操作',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
link: 'common-ui/vben-cropper',
|
link: 'common-ui/vben-cropper',
|
||||||
text: 'Cropper 图片裁剪',
|
text: 'Cropper 图片裁剪',
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@lucide/vue": "catalog:",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben/common-ui": "workspace:*",
|
"@vben/common-ui": "workspace:*",
|
||||||
"@vben/locales": "workspace:*",
|
"@vben/locales": "workspace:*",
|
||||||
"@vben/plugins": "workspace:*",
|
"@vben/plugins": "workspace:*",
|
||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"antdv-next": "catalog:",
|
"antdv-next": "catalog:",
|
||||||
"lucide-vue-next": "catalog:",
|
|
||||||
"medium-zoom": "catalog:",
|
"medium-zoom": "catalog:",
|
||||||
"reka-ui": "catalog:",
|
"reka-ui": "catalog:",
|
||||||
"vitepress-plugin-group-icons": "catalog:"
|
"vitepress-plugin-group-icons": "catalog:"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vben Descriptions 描述列表
|
||||||
|
|
||||||
|
`Descriptions` 用于成组展示只读的字段信息,常用于详情页、信息预览等场景。组件基于 shadcn-ui 构建,API 参考 Ant Design Vue 的 Descriptions,支持响应式列数、跨列、边框、垂直布局等能力。
|
||||||
|
|
||||||
|
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
|
||||||
|
|
||||||
|
::: info 写在前面
|
||||||
|
|
||||||
|
组件提供两种使用方式:通过 `items` 数据驱动(推荐),或通过子组件 `VbenDescriptionsItem` 声明列表项。两者可按需选择,`items` 优先级更高。:::
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
通过 `items` 传入字段数组,每项包含 `label` 与 `content`。默认按断点自适应列数(`xs` 1 列、`sm` 2 列、`md` 及以上 3 列)。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/basic" />
|
||||||
|
|
||||||
|
## 带边框
|
||||||
|
|
||||||
|
设置 `bordered` 展示边框样式,配合 `title` 标题与 `#extra` 插槽(位于标题右侧的操作区域)。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/bordered" />
|
||||||
|
|
||||||
|
## 垂直布局
|
||||||
|
|
||||||
|
通过 `layout="vertical"` 让标签位于内容上方。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/vertical" />
|
||||||
|
|
||||||
|
## 不同尺寸
|
||||||
|
|
||||||
|
通过 `size` 设置 `small`、`middle`、`large` 三种尺寸。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/size" />
|
||||||
|
|
||||||
|
## 跨列与响应式
|
||||||
|
|
||||||
|
单项通过 `span` 设置跨列数,`'filled'` 表示占满当前行剩余空间;`column` 支持传入按断点配置的对象实现响应式列数。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/span" />
|
||||||
|
|
||||||
|
## 子组件用法
|
||||||
|
|
||||||
|
不传 `items` 时,可在默认插槽中使用 `VbenDescriptionsItem` 声明列表项,内容支持默认插槽或 `#content` 插槽自定义。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/custom" />
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Descriptions Props
|
||||||
|
|
||||||
|
| 属性名 | 描述 | 类型 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| items | 数据驱动的列表项;不传则读取默认插槽 | `DescriptionsItemType[]` | - |
|
||||||
|
| bordered | 是否展示边框 | `boolean` | `false` |
|
||||||
|
| column | 一行的列数,支持按断点配置 | `number \| Partial<Record<Breakpoint, number>>` | `{ xs: 1, sm: 2, md: 3, xxxl: 4 }` |
|
||||||
|
| layout | 布局方式 | `'horizontal' \| 'vertical'` | `'horizontal'` |
|
||||||
|
| size | 尺寸 | `'small' \| 'middle' \| 'large'` | `'middle'` |
|
||||||
|
| colon | 是否显示冒号(仅非边框的水平布局生效) | `boolean` | `true` |
|
||||||
|
| title | 标题 | `string` | - |
|
||||||
|
| extra | 标题右侧的操作区域 | `string` | - |
|
||||||
|
| labelStyle | 统一的标签样式 | `CSSProperties` | - |
|
||||||
|
| contentStyle | 统一的内容样式 | `CSSProperties` | - |
|
||||||
|
| class | 根节点自定义类名 | `string` | - |
|
||||||
|
|
||||||
|
### Descriptions Slots
|
||||||
|
|
||||||
|
| 插槽名 | 描述 |
|
||||||
|
| ------- | ---------------------------------- |
|
||||||
|
| title | 自定义标题 |
|
||||||
|
| extra | 自定义标题右侧操作区域 |
|
||||||
|
| default | 放置 `VbenDescriptionsItem` 子组件 |
|
||||||
|
|
||||||
|
### DescriptionsItem
|
||||||
|
|
||||||
|
`items` 数组中的每一项,或子组件 `VbenDescriptionsItem` 的属性。
|
||||||
|
|
||||||
|
| 属性名 | 描述 | 类型 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| label | 标签 | `string \| number \| (() => VNode) \| Component` | - |
|
||||||
|
| content | 内容 | `string \| number \| (() => VNode) \| Component` | - |
|
||||||
|
| span | 跨列数,`'filled'` 占满当前行剩余 | `number \| 'filled' \| Partial<Record<Breakpoint, number>>` | `1` |
|
||||||
|
| labelStyle | 标签样式 | `CSSProperties` | - |
|
||||||
|
| contentStyle | 内容样式 | `CSSProperties` | - |
|
||||||
|
| key | 唯一标识 | `string \| number` | - |
|
||||||
|
|
||||||
|
### DescriptionsItem Slots
|
||||||
|
|
||||||
|
仅子组件用法可用。
|
||||||
|
|
||||||
|
| 插槽名 | 描述 |
|
||||||
|
| ------- | ------------------------ |
|
||||||
|
| default | 内容(等价于 `content`) |
|
||||||
|
| content | 自定义内容 |
|
||||||
|
| label | 自定义标签 |
|
||||||
|
|
||||||
|
::: tip Breakpoint
|
||||||
|
|
||||||
|
响应式断点 `Breakpoint` 取值为 `'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'`,断点像素与 Ant Design 一致(`sm` 576、`md` 768、`lg` 992、`xl` 1200、`xxl` 1600、`xxxl` 2000)。:::
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vben TableAction 表格操作
|
||||||
|
|
||||||
|
`TableAction` 用于在表格操作列中渲染一组操作按钮,参考 vben2 的 TableAction 设计。基于 shadcn-ui 构建,支持权限控制、气泡确认、提示、下拉「更多」、分割线等能力,可在表格内外任意场景复用。
|
||||||
|
|
||||||
|
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
|
||||||
|
|
||||||
|
::: info 写在前面
|
||||||
|
|
||||||
|
组件本身不依赖任何业务逻辑(不直接读取权限 store),权限通过注入 `hasPermission` 实现,从而保持核心层零耦合、可跨框架复用。在 vxe-table 中推荐通过列插槽(`slots: { default: 'action' }`)在页面里渲染,不改变表格原有的渲染机制。:::
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
通过 `actions` 传入操作项数组,每项包含 `text`、`onClick` 等;`danger` 标记危险操作,`divider` 显示按钮间分割线。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/basic" />
|
||||||
|
|
||||||
|
## 提示
|
||||||
|
|
||||||
|
通过 `tooltip` 为操作项添加提示,支持字符串或 `{ content, side }` 配置。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/tooltip" />
|
||||||
|
|
||||||
|
## 气泡确认
|
||||||
|
|
||||||
|
通过 `popConfirm` 开启点击前的气泡确认,常用于删除等危险操作。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/popconfirm" />
|
||||||
|
|
||||||
|
## 更多下拉
|
||||||
|
|
||||||
|
通过 `dropdownActions` 将次要操作收纳到「更多」下拉中,`moreText` 可自定义按钮文案。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/dropdown" />
|
||||||
|
|
||||||
|
## 权限控制
|
||||||
|
|
||||||
|
为操作项设置 `auth` 权限码,并注入 `hasPermission` 判断函数,无权限的操作会被隐藏。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/permission" />
|
||||||
|
|
||||||
|
## 在 vxe-table 中使用
|
||||||
|
|
||||||
|
不改变 vxe-table 原有渲染方式,推荐在列配置中声明插槽,在页面通过插槽渲染。
|
||||||
|
|
||||||
|
::: tip 推荐:使用适配器封装的版本项目的 `#/adapter/vxe-table` 已对 `VbenTableAction` 做了二次封装,内部统一注入了 `hasPermission`(基于 `useAccess().hasAccessByCodes`)。因此从适配器引入时**无需再传入 `:has-permission`**,只需通过操作项的 `auth` 字段声明权限码即可。:::
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// data.ts —— 列配置声明插槽
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
field: 'operation',
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'action' },
|
||||||
|
title: $t('system.user.operation'),
|
||||||
|
width: 180,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- list.vue —— 从适配器引入,权限自动注入,无需传入 has-permission -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { VbenTableAction } from '#/adapter/vxe-table';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Grid>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<template #action="{ row }">
|
||||||
|
<VbenTableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
text: $t('common.detail'),
|
||||||
|
icon: 'lucide:eye',
|
||||||
|
onClick: () => onDetail(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: $t('common.edit'),
|
||||||
|
icon: 'lucide:edit',
|
||||||
|
onClick: () => onEdit(row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:dropdown-actions="[
|
||||||
|
{
|
||||||
|
text: $t('common.delete'),
|
||||||
|
icon: 'lucide:trash-2',
|
||||||
|
danger: true,
|
||||||
|
onClick: () => onDelete(row),
|
||||||
|
auth: ['AC_100100'],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
若直接从 `@vben/common-ui` 引入核心组件(不经过适配器),组件不依赖任何业务逻辑,需自行注入 `hasPermission`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { VbenTableAction } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
function hasPermission(auth?: string | string[]) {
|
||||||
|
if (!auth) return true;
|
||||||
|
return hasAccessByCodes(Array.isArray(auth) ? auth : [auth]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VbenTableAction
|
||||||
|
v-bind="useActions(row, onActionClick)"
|
||||||
|
:has-permission="hasPermission"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### TableAction Props
|
||||||
|
|
||||||
|
| 属性名 | 描述 | 类型 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| actions | 主操作按钮 | `ActionItem[]` | `[]` |
|
||||||
|
| dropdownActions | 「更多」下拉中的操作 | `ActionItem[]` | `[]` |
|
||||||
|
| align | 对齐方式 | `'start' \| 'center' \| 'end'` | `'end'` |
|
||||||
|
| divider | 按钮之间是否显示分割线 | `boolean` | `false` |
|
||||||
|
| moreText | 「更多」按钮文案(提供时显示在图标右侧) | `string` | - |
|
||||||
|
| hasPermission | 权限判断函数,返回 `false` 则隐藏对应 `auth` 的操作(从 `#/adapter/vxe-table` 引入时已自动注入,无需手动传入) | `(auth?: string \| string[]) => boolean` | - |
|
||||||
|
| class | 根节点自定义类名 | `string` | - |
|
||||||
|
|
||||||
|
### ActionItem
|
||||||
|
|
||||||
|
| 属性名 | 描述 | 类型 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| text | 按钮文本 | `string` | - |
|
||||||
|
| icon | 图标组件 | `string`\| `VbenIcon` | - |
|
||||||
|
| onClick | 点击回调 | `() => void` | - |
|
||||||
|
| auth | 权限码,配合 `hasPermission` 过滤 | `string \| string[]` | - |
|
||||||
|
| ifShow | 是否显示 | `boolean \| (() => boolean)` | `true` |
|
||||||
|
| disabled | 是否禁用 | `boolean` | `false` |
|
||||||
|
| loading | 加载状态 | `boolean` | `false` |
|
||||||
|
| danger | 危险操作(红色文字) | `boolean` | `false` |
|
||||||
|
| tooltip | 提示 | `string \| { content: string; side?: 'top' \| 'bottom' \| 'left' \| 'right' }` | - |
|
||||||
|
| popConfirm | 气泡确认 | `TableActionPopConfirm` | - |
|
||||||
|
| variant | 按钮样式变体 | `ButtonVariants['variant']` | `'link'` |
|
||||||
|
| size | 按钮尺寸 | `ButtonVariants['size']` | `'sm'` |
|
||||||
|
| key | 唯一标识 | `string \| number` | - |
|
||||||
|
|
||||||
|
### TableActionPopConfirm
|
||||||
|
|
||||||
|
| 属性名 | 描述 | 类型 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| title | 提示标题 | `string` | `'Are you sure?'` |
|
||||||
|
| okText | 确认按钮文案 | `string` | `'OK'` |
|
||||||
|
| cancelText | 取消按钮文案 | `string` | `'Cancel'` |
|
||||||
|
| confirm | 确认回调;未提供时回退到 `action.onClick` | `() => void` | - |
|
||||||
|
|
@ -3,8 +3,8 @@ import { h } from 'vue';
|
||||||
|
|
||||||
import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
|
import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { BadgeJapaneseYen } from '@lucide/vue';
|
||||||
import { Input, RadioGroup, Select } from 'antdv-next';
|
import { Input, RadioGroup, Select } from 'antdv-next';
|
||||||
import { BadgeJapaneseYen } from 'lucide-vue-next';
|
|
||||||
|
|
||||||
function showPrompt() {
|
function showPrompt() {
|
||||||
prompt({
|
prompt({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VbenDescriptions } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ content: 'Vben', label: '用户名' },
|
||||||
|
{ content: '13800138000', label: '手机号' },
|
||||||
|
{ content: '中国 · 杭州', label: '居住地' },
|
||||||
|
{ content: '前端工程师', label: '职位' },
|
||||||
|
{
|
||||||
|
content: '这是一段较长的备注信息,用于演示跨列展示。',
|
||||||
|
label: '备注',
|
||||||
|
span: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VbenDescriptions :items="items" />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VbenDescriptions } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ content: 'Vben', label: '用户名' },
|
||||||
|
{ content: '13800138000', label: '手机号' },
|
||||||
|
{ content: '正常', label: '状态' },
|
||||||
|
{ content: '中国 · 杭州', label: '居住地' },
|
||||||
|
{
|
||||||
|
content: '浙江省杭州市西湖区某某街道某某小区 1 幢 2 单元',
|
||||||
|
label: '地址',
|
||||||
|
span: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VbenDescriptions bordered title="用户信息" :items="items">
|
||||||
|
<template #extra>
|
||||||
|
<span style="color: #1677ff; cursor: pointer">编辑</span>
|
||||||
|
</template>
|
||||||
|
</VbenDescriptions>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VbenDescriptions, VbenDescriptionsItem } from '@vben/common-ui';
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<!-- 通过子组件 VbenDescriptionsItem 声明列表项 -->
|
||||||
|
<VbenDescriptions bordered :column="2">
|
||||||
|
<VbenDescriptionsItem label="用户名">Vben</VbenDescriptionsItem>
|
||||||
|
<VbenDescriptionsItem label="状态">
|
||||||
|
<span style="color: #52c41a">● 正常</span>
|
||||||
|
</VbenDescriptionsItem>
|
||||||
|
<VbenDescriptionsItem label="备注" :span="2">
|
||||||
|
<template #content>
|
||||||
|
<span style="color: #888">通过 #content 插槽自定义内容</span>
|
||||||
|
</template>
|
||||||
|
</VbenDescriptionsItem>
|
||||||
|
</VbenDescriptions>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VbenDescriptions } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ content: 'Vben', label: '用户名' },
|
||||||
|
{ content: '13800138000', label: '手机号' },
|
||||||
|
{ content: '中国 · 杭州', label: '居住地' },
|
||||||
|
{ content: '前端工程师', label: '职位' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 16px">
|
||||||
|
<VbenDescriptions
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
title="Small"
|
||||||
|
:column="2"
|
||||||
|
:items="items"
|
||||||
|
/>
|
||||||
|
<VbenDescriptions
|
||||||
|
size="middle"
|
||||||
|
bordered
|
||||||
|
title="Middle"
|
||||||
|
:column="2"
|
||||||
|
:items="items"
|
||||||
|
/>
|
||||||
|
<VbenDescriptions
|
||||||
|
size="large"
|
||||||
|
bordered
|
||||||
|
title="Large"
|
||||||
|
:column="2"
|
||||||
|
:items="items"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VbenDescriptions } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ content: '1', label: 'A' },
|
||||||
|
{ content: '2(span: 2)', label: 'B', span: 2 },
|
||||||
|
{ content: '3', label: 'C' },
|
||||||
|
{ content: '占满当前行剩余空间', label: 'D(span: filled)', span: 'filled' },
|
||||||
|
{ content: '5', label: 'E' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<!-- 列数随断点变化:xs 1 列、sm 2 列、md 及以上 3 列 -->
|
||||||
|
<VbenDescriptions bordered :column="{ md: 3, sm: 2, xs: 1 }" :items="items" />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VbenDescriptions } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ content: 'Vben', label: '用户名' },
|
||||||
|
{ content: '13800138000', label: '手机号' },
|
||||||
|
{ content: '中国 · 杭州', label: '居住地' },
|
||||||
|
{ content: '这是一段较长的备注信息。', label: '备注', span: 3 },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VbenDescriptions bordered layout="vertical" :items="items" />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ActionItem } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { VbenTableAction } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const last = ref('无');
|
||||||
|
|
||||||
|
const actions: ActionItem[] = [
|
||||||
|
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
|
||||||
|
{ key: 'detail', onClick: () => (last.value = '详情'), text: '详情' },
|
||||||
|
{
|
||||||
|
danger: true,
|
||||||
|
key: 'delete',
|
||||||
|
onClick: () => (last.value = '删除'),
|
||||||
|
text: '删除',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VbenTableAction :actions="actions" align="start" divider />
|
||||||
|
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
|
||||||
|
最近点击:{{ last }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ActionItem } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { VbenTableAction } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const last = ref('无');
|
||||||
|
|
||||||
|
const actions: ActionItem[] = [
|
||||||
|
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const dropdownActions: ActionItem[] = [
|
||||||
|
{ key: 'copy', onClick: () => (last.value = '复制'), text: '复制' },
|
||||||
|
{ key: 'export', onClick: () => (last.value = '导出'), text: '导出' },
|
||||||
|
{
|
||||||
|
danger: true,
|
||||||
|
key: 'remove',
|
||||||
|
// 下拉项同样支持气泡确认
|
||||||
|
popConfirm: {
|
||||||
|
cancelText: '取消',
|
||||||
|
confirm: () => (last.value = '已移除'),
|
||||||
|
okText: '确认',
|
||||||
|
title: '确定移除吗?',
|
||||||
|
},
|
||||||
|
text: '移除',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VbenTableAction
|
||||||
|
:actions="actions"
|
||||||
|
:dropdown-actions="dropdownActions"
|
||||||
|
align="start"
|
||||||
|
divider
|
||||||
|
more-text="更多"
|
||||||
|
/>
|
||||||
|
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
|
||||||
|
最近点击:{{ last }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ActionItem } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { VbenTableAction } from '@vben/common-ui';
|
||||||
|
|
||||||
|
// 模拟当前用户拥有的权限码
|
||||||
|
const allow = new Set(['user:detail', 'user:edit']);
|
||||||
|
|
||||||
|
function hasPermission(auth?: string | string[]) {
|
||||||
|
if (!auth) return true;
|
||||||
|
const codes = Array.isArray(auth) ? auth : [auth];
|
||||||
|
return codes.some((code) => allow.has(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: ActionItem[] = [
|
||||||
|
{ auth: 'user:edit', key: 'edit', text: '编辑' },
|
||||||
|
{ auth: 'user:detail', key: 'detail', text: '详情' },
|
||||||
|
// 无 user:delete 权限,按钮被隐藏
|
||||||
|
{ auth: 'user:delete', danger: true, key: 'delete', text: '删除(无权限)' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VbenTableAction
|
||||||
|
:actions="actions"
|
||||||
|
:has-permission="hasPermission"
|
||||||
|
align="start"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ActionItem } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { VbenTableAction } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const last = ref('无');
|
||||||
|
|
||||||
|
const actions: ActionItem[] = [
|
||||||
|
{ key: 'edit', onClick: () => (last.value = '编辑'), text: '编辑' },
|
||||||
|
{
|
||||||
|
danger: true,
|
||||||
|
key: 'delete',
|
||||||
|
popConfirm: {
|
||||||
|
cancelText: '取消',
|
||||||
|
confirm: () => (last.value = '已删除'),
|
||||||
|
okText: '确认',
|
||||||
|
title: '确定删除这一行吗?',
|
||||||
|
},
|
||||||
|
text: '删除',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VbenTableAction :actions="actions" align="start" />
|
||||||
|
<p style="margin-top: 8px; font-size: 13px; opacity: 0.7">
|
||||||
|
最近操作:{{ last }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ActionItem } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { VbenTableAction } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const actions: ActionItem[] = [
|
||||||
|
{ key: 'edit', text: '编辑', tooltip: '编辑这一行' },
|
||||||
|
{
|
||||||
|
key: 'detail',
|
||||||
|
text: '详情',
|
||||||
|
tooltip: { content: '查看详情', side: 'top' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VbenTableAction :actions="actions" align="start" />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vben Descriptions
|
||||||
|
|
||||||
|
`Descriptions` displays a group of read-only fields, commonly used on detail pages and information previews. It is built on shadcn-ui with an API modeled after Ant Design Vue's Descriptions, supporting responsive columns, column spanning, borders, and vertical layout.
|
||||||
|
|
||||||
|
> If the documentation does not cover the details you need, please refer to the online examples.
|
||||||
|
|
||||||
|
::: info Before you start
|
||||||
|
|
||||||
|
The component supports two usages: data-driven via `items` (recommended), or declaring entries with the `VbenDescriptionsItem` child component. `items` takes precedence when both are provided. :::
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
Pass an array of fields via `items`, each with a `label` and `content`. Columns adapt to breakpoints by default (1 column on `xs`, 2 on `sm`, 3 on `md` and above).
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/basic" />
|
||||||
|
|
||||||
|
## Bordered
|
||||||
|
|
||||||
|
Set `bordered` for a bordered style, combined with the `title` prop and the `#extra` slot (an action area on the right of the title).
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/bordered" />
|
||||||
|
|
||||||
|
## Vertical Layout
|
||||||
|
|
||||||
|
Use `layout="vertical"` to place labels above their content.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/vertical" />
|
||||||
|
|
||||||
|
## Sizes
|
||||||
|
|
||||||
|
Use `size` to switch between `small`, `middle`, and `large`.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/size" />
|
||||||
|
|
||||||
|
## Span & Responsive
|
||||||
|
|
||||||
|
Set `span` on an item to span multiple columns; `'filled'` fills the remaining space of the current row. `column` accepts a breakpoint-keyed object for responsive columns.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/span" />
|
||||||
|
|
||||||
|
## Child Component Usage
|
||||||
|
|
||||||
|
When `items` is omitted, declare entries with `VbenDescriptionsItem` in the default slot. Content can be customized via the default slot or the `#content` slot.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-descriptions/custom" />
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Descriptions Props
|
||||||
|
|
||||||
|
| Prop | Description | Type | Default |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| items | Data-driven entries; reads the default slot when omitted | `DescriptionsItemType[]` | - |
|
||||||
|
| bordered | Whether to show borders | `boolean` | `false` |
|
||||||
|
| column | Columns per row, supports breakpoint config | `number \| Partial<Record<Breakpoint, number>>` | `{ xs: 1, sm: 2, md: 3, xxxl: 4 }` |
|
||||||
|
| layout | Layout direction | `'horizontal' \| 'vertical'` | `'horizontal'` |
|
||||||
|
| size | Size | `'small' \| 'middle' \| 'large'` | `'middle'` |
|
||||||
|
| colon | Show colon (only for non-bordered horizontal layout) | `boolean` | `true` |
|
||||||
|
| title | Title | `string` | - |
|
||||||
|
| extra | Action area on the right of the title | `string` | - |
|
||||||
|
| labelStyle | Shared label style | `CSSProperties` | - |
|
||||||
|
| contentStyle | Shared content style | `CSSProperties` | - |
|
||||||
|
| class | Custom class for the root node | `string` | - |
|
||||||
|
|
||||||
|
### Descriptions Slots
|
||||||
|
|
||||||
|
| Slot | Description |
|
||||||
|
| ------- | ------------------------------------- |
|
||||||
|
| title | Custom title |
|
||||||
|
| extra | Custom action area beside the title |
|
||||||
|
| default | Place `VbenDescriptionsItem` children |
|
||||||
|
|
||||||
|
### DescriptionsItem
|
||||||
|
|
||||||
|
Each entry in `items`, or the props of the `VbenDescriptionsItem` child component.
|
||||||
|
|
||||||
|
| Prop | Description | Type | Default |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| label | Label | `string \| number \| (() => VNode) \| Component` | - |
|
||||||
|
| content | Content | `string \| number \| (() => VNode) \| Component` | - |
|
||||||
|
| span | Columns to span, `'filled'` fills the rest of the row | `number \| 'filled' \| Partial<Record<Breakpoint, number>>` | `1` |
|
||||||
|
| labelStyle | Label style | `CSSProperties` | - |
|
||||||
|
| contentStyle | Content style | `CSSProperties` | - |
|
||||||
|
| key | Unique key | `string \| number` | - |
|
||||||
|
|
||||||
|
### DescriptionsItem Slots
|
||||||
|
|
||||||
|
Available only for the child component usage.
|
||||||
|
|
||||||
|
| Slot | Description |
|
||||||
|
| ------- | --------------------------------- |
|
||||||
|
| default | Content (equivalent to `content`) |
|
||||||
|
| content | Custom content |
|
||||||
|
| label | Custom label |
|
||||||
|
|
||||||
|
::: tip Breakpoint
|
||||||
|
|
||||||
|
The responsive `Breakpoint` is one of `'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'`, with pixel values aligned with Ant Design (`sm` 576, `md` 768, `lg` 992, `xl` 1200, `xxl` 1600, `xxxl` 2000). :::
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vben TableAction
|
||||||
|
|
||||||
|
`TableAction` renders a group of action buttons for table operation columns, inspired by the TableAction component from vben2. Built on shadcn-ui, it supports permission control, popconfirm, tooltips, a "more" dropdown, and dividers, and can be reused inside or outside tables.
|
||||||
|
|
||||||
|
> If the documentation does not cover the details you need, please refer to the online examples.
|
||||||
|
|
||||||
|
::: info Before you start
|
||||||
|
|
||||||
|
The component carries no business logic (it does not read the permission store directly); permissions are handled by injecting `hasPermission`, keeping the core layer decoupled and reusable across frameworks. Inside vxe-table, the recommended approach is to render it via a column slot (`slots: { default: 'action' }`) on the page, without changing the table's original rendering mechanism. :::
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
Pass an array of action items via `actions`, each with `text`, `onClick`, etc. `danger` marks destructive actions, and `divider` shows separators between buttons.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/basic" />
|
||||||
|
|
||||||
|
## Tooltip
|
||||||
|
|
||||||
|
Add a tooltip to an action via `tooltip`, accepting a string or a `{ content, side }` object.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/tooltip" />
|
||||||
|
|
||||||
|
## PopConfirm
|
||||||
|
|
||||||
|
Use `popConfirm` to require confirmation before the action runs, commonly used for destructive actions like delete.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/popconfirm" />
|
||||||
|
|
||||||
|
## More Dropdown
|
||||||
|
|
||||||
|
Use `dropdownActions` to collapse secondary actions into a "more" dropdown. `moreText` customizes the button label.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/dropdown" />
|
||||||
|
|
||||||
|
## Permission Control
|
||||||
|
|
||||||
|
Set an `auth` code on an action and inject a `hasPermission` resolver; actions without permission are hidden.
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-table-action/permission" />
|
||||||
|
|
||||||
|
## Usage with vxe-table
|
||||||
|
|
||||||
|
Without changing vxe-table's rendering mechanism, declare a slot in the column config and render it on the page.
|
||||||
|
|
||||||
|
::: tip Recommended: use the adapter-wrapped version The project's `#/adapter/vxe-table` re-wraps `VbenTableAction` and injects `hasPermission` internally (based on `useAccess().hasAccessByCodes`). So when you import it from the adapter, **you no longer need to pass `:has-permission`** — just declare permission codes via the `auth` field of each action. :::
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// data.ts — declare a slot in the column config
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
field: 'operation',
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'action' },
|
||||||
|
title: $t('system.user.operation'),
|
||||||
|
width: 180,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- list.vue — import from the adapter; permission is auto-injected, no has-permission needed -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { VbenTableAction } from '#/adapter/vxe-table';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Grid>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<template #action="{ row }">
|
||||||
|
<VbenTableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
text: $t('common.detail'),
|
||||||
|
icon: 'lucide:eye',
|
||||||
|
onClick: () => onDetail(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: $t('common.edit'),
|
||||||
|
icon: 'lucide:edit',
|
||||||
|
onClick: () => onEdit(row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:dropdown-actions="[
|
||||||
|
{
|
||||||
|
text: $t('common.delete'),
|
||||||
|
icon: 'lucide:trash-2',
|
||||||
|
danger: true,
|
||||||
|
onClick: () => onDelete(row),
|
||||||
|
auth: ['AC_100100'],
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
If you import the core component directly from `@vben/common-ui` (without going through the adapter), the component carries no business logic and you need to inject `hasPermission` yourself:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { VbenTableAction } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
function hasPermission(auth?: string | string[]) {
|
||||||
|
if (!auth) return true;
|
||||||
|
return hasAccessByCodes(Array.isArray(auth) ? auth : [auth]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VbenTableAction
|
||||||
|
v-bind="useActions(row, onActionClick)"
|
||||||
|
:has-permission="hasPermission"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### TableAction Props
|
||||||
|
|
||||||
|
| Prop | Description | Type | Default |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| actions | Main action buttons | `ActionItem[]` | `[]` |
|
||||||
|
| dropdownActions | Actions inside the "more" dropdown | `ActionItem[]` | `[]` |
|
||||||
|
| align | Alignment | `'start' \| 'center' \| 'end'` | `'end'` |
|
||||||
|
| divider | Whether to show separators between buttons | `boolean` | `false` |
|
||||||
|
| moreText | Label for the "more" button (shown beside the icon) | `string` | - |
|
||||||
|
| hasPermission | Permission resolver; returning `false` hides the action with that `auth` (auto-injected when imported from `#/adapter/vxe-table`, no need to pass manually) | `(auth?: string \| string[]) => boolean` | - |
|
||||||
|
| class | Custom class for the root node | `string` | - |
|
||||||
|
|
||||||
|
### ActionItem
|
||||||
|
|
||||||
|
| Prop | Description | Type | Default |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| text | Button text | `string` | - |
|
||||||
|
| icon | Icon component | `string` \| `VbenIcon` | - |
|
||||||
|
| onClick | Click callback | `() => void` | - |
|
||||||
|
| auth | Permission code, filtered by `hasPermission` | `string \| string[]` | - |
|
||||||
|
| ifShow | Whether to show | `boolean \| (() => boolean)` | `true` |
|
||||||
|
| disabled | Whether disabled | `boolean` | `false` |
|
||||||
|
| loading | Loading state | `boolean` | `false` |
|
||||||
|
| danger | Destructive action (red text) | `boolean` | `false` |
|
||||||
|
| tooltip | Tooltip | `string \| { content: string; side?: 'top' \| 'bottom' \| 'left' \| 'right' }` | - |
|
||||||
|
| popConfirm | PopConfirm | `TableActionPopConfirm` | - |
|
||||||
|
| variant | Button variant | `ButtonVariants['variant']` | `'link'` |
|
||||||
|
| size | Button size | `ButtonVariants['size']` | `'sm'` |
|
||||||
|
| key | Unique key | `string \| number` | - |
|
||||||
|
|
||||||
|
### TableActionPopConfirm
|
||||||
|
|
||||||
|
| Prop | Description | Type | Default |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| title | Confirm title | `string` | `'Are you sure?'` |
|
||||||
|
| okText | Confirm button text | `string` | `'OK'` |
|
||||||
|
| cancelText | Cancel button text | `string` | `'Cancel'` |
|
||||||
|
| confirm | Confirm callback; falls back to `action.onClick` if omitted | `() => void` | - |
|
||||||
|
|
@ -98,7 +98,7 @@ VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||||
|
|
||||||
::: tip How to Dynamically Modify API Endpoint in Production
|
::: tip How to Dynamically Modify API Endpoint in Production
|
||||||
|
|
||||||
Variables starting with `VITE_GLOB_*` in the `.env` file are injected into the `_app.config.js` file during packaging. After packaging, you can modify the corresponding API addresses in `dist/_app.config.js` and refresh the page to apply the changes. This eliminates the need to package multiple times for different environments, allowing a single package to be deployed across multiple API environments.
|
Variables starting with `VITE_GLOB_*` in the `.env` file are injected into the `_app-config-{version}-{hash}.js` file during packaging. After packaging, you can modify the corresponding API addresses in `dist/_app-config-{version}-{hash}.js` and refresh the page to apply the changes. This eliminates the need to package multiple times for different environments, allowing a single package to be deployed across multiple API environments.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
|
||||||
console.log(import.meta.env.VITE_PROT);
|
console.log(import.meta.env.VITE_PROT);
|
||||||
```
|
```
|
||||||
|
|
||||||
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
|
- Variables starting with `VITE_GLOB_*` will be added to the `_app-config-{version}-{hash}.js` configuration file during packaging.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
@ -87,9 +87,9 @@ VITE_ARCHIVER=true
|
||||||
|
|
||||||
## Dynamic Configuration in Production Environment
|
## Dynamic Configuration in Production Environment
|
||||||
|
|
||||||
When executing `pnpm build` in the root directory of the monorepo, a `dist/_app.config.js` file will be automatically generated in the corresponding application and inserted into `index.html`.
|
When executing `pnpm build` in the root directory of the monorepo, a `dist/_app-config-{version}-{hash}.js` file will be automatically generated in the corresponding application and inserted into `index.html`.
|
||||||
|
|
||||||
`_app.config.js` is a dynamic configuration file that allows for modifications to the configuration dynamically based on different environments after the project has been built. The content is as follows:
|
`_app-config-{version}-{hash}.js` is a dynamic configuration file that allows for modifications to the configuration dynamically based on different environments after the project has been built. The content is as follows:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
window._VBEN_ADMIN_PRO_APP_CONF_ = {
|
window._VBEN_ADMIN_PRO_APP_CONF_ = {
|
||||||
|
|
@ -104,11 +104,11 @@ Object.defineProperty(window, '_VBEN_ADMIN_PRO_APP_CONF_', {
|
||||||
|
|
||||||
### Purpose
|
### Purpose
|
||||||
|
|
||||||
`_app.config.js` is used for projects that need to dynamically modify configurations after packaging, such as API endpoints. There's no need to repackage; you can simply modify the variables in `/dist/_app.config.js` after packaging, and refresh to update the variables in the code. A `js` file is used to ensure that the configuration file is loaded early in the order.
|
`_app-config-{version}-{hash}.js` is used for projects that need to dynamically modify configurations after packaging, such as API endpoints. There's no need to repackage; you can simply modify the variables in `/dist/_app-config-{version}-{hash}.js` after packaging, and refresh to update the variables in the code. A `js` file is used to ensure that the configuration file is loaded early in the order.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
To access the variables inside `_app.config.js`, you need to use the `useAppConfig` method provided by `@vben/hooks`.
|
To access the variables inside `_app-config-{version}-{hash}.js`, you need to use the `useAppConfig` method provided by `@vben/hooks`.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
## Browser Support
|
## Browser Support
|
||||||
|
|
||||||
- **Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
|
- **Local development** is recommended using the **latest version of Chrome**. **Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+**.
|
||||||
|
|
||||||
- **Production environment** supports modern browsers, IE is not supported.
|
- **Production environment** supports modern browsers, IE is not supported.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||||
|
|
||||||
::: tip 打包如何动态修改接口地址
|
::: tip 打包如何动态修改接口地址
|
||||||
|
|
||||||
`.env` 文件内的 `VITE_GLOB_*` 开头的变量会在打包的时候注入 `_app.config.js` 文件内。在 `dist/_app.config.js` 修改相应的接口地址后刷新页面即可,不需要在根据不同环境打包多次,一次打包可以用于多个不同接口环境的部署。
|
`.env` 文件内的 `VITE_GLOB_*` 开头的变量会在打包的时候注入 `_app-config-{version}-{hash}.js` 文件内。在 `dist/_app-config-{version}-{hash}.js` 修改相应的接口地址后刷新页面即可,不需要在根据不同环境打包多次,一次打包可以用于多个不同接口环境的部署。
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
console.log(import.meta.env.VITE_PROT);
|
console.log(import.meta.env.VITE_PROT);
|
||||||
```
|
```
|
||||||
|
|
||||||
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中.
|
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app-config-{version}-{hash}.js`配置文件当中.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
@ -86,9 +86,9 @@ VITE_ARCHIVER=true
|
||||||
|
|
||||||
## 生产环境动态配置
|
## 生产环境动态配置
|
||||||
|
|
||||||
当在大仓根目录下,执行 `pnpm build`构建项目之后,会自动在对应的应用下生成 `dist/_app.config.js`文件并插入 `index.html`。
|
当在大仓根目录下,执行 `pnpm build`构建项目之后,会自动在对应的应用下生成 `dist/_app-config-{version}-{hash}.js`文件并插入 `index.html`。
|
||||||
|
|
||||||
`_app.config.js` 是一个动态配置文件,可以在项目构建之后,根据不同的环境动态修改配置。内容如下:
|
`_app-config-{version}-{hash}.js` 是一个动态配置文件,可以在项目构建之后,根据不同的环境动态修改配置。内容如下:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
window._VBEN_ADMIN_PRO_APP_CONF_ = {
|
window._VBEN_ADMIN_PRO_APP_CONF_ = {
|
||||||
|
|
@ -103,11 +103,11 @@ Object.defineProperty(window, '_VBEN_ADMIN_PRO_APP_CONF_', {
|
||||||
|
|
||||||
### 作用
|
### 作用
|
||||||
|
|
||||||
`_app.config.js` 用于项目在打包后,需要动态修改配置的需求,如接口地址。不用重新进行打包,可在打包后修改 /`dist/_app.config.js` 内的变量,刷新即可更新代码内的局部变量。这里使用`js`文件,是为了确保配置文件加载顺序保持在前面。
|
`_app-config-{version}-{hash}.js` 用于项目在打包后,需要动态修改配置的需求,如接口地址。不用重新进行打包,可在打包后修改 /`dist/_app-config-{version}-{hash}.js` 内的变量,刷新即可更新代码内的局部变量。这里使用`js`文件,是为了确保配置文件加载顺序保持在前面。
|
||||||
|
|
||||||
### 使用
|
### 使用
|
||||||
|
|
||||||
想要获取 `_app.config.js` 内的变量,需要使用`@vben/hooks`提供的 `useAppConfig`方法。
|
想要获取 `_app-config-{version}-{hash}.js` 内的变量,需要使用`@vben/hooks`提供的 `useAppConfig`方法。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
## 浏览器支持
|
## 浏览器支持
|
||||||
|
|
||||||
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
|
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**` Tailwind CSS v4.0 is designed for Safari 16.4+, Chrome 111+, and Firefox 128+。
|
||||||
|
|
||||||
- **生产环境**支持现代浏览器,不支持 IE。
|
- **生产环境**支持现代浏览器,不支持 IE。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// eslint-disable-next-line n/no-extraneous-import
|
||||||
|
import type { UserConfig } from '@commitlint/types';
|
||||||
|
|
||||||
|
declare const userConfig: UserConfig;
|
||||||
|
|
||||||
|
export default userConfig;
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"module": "./index.mjs",
|
"module": "./index.mjs",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
"import": "./index.mjs",
|
"import": "./index.mjs",
|
||||||
"default": "./index.mjs"
|
"default": "./index.mjs"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export async function node(): Promise<Linter.Config[]> {
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
ignores: [],
|
ignores: [],
|
||||||
version: '>=20.12.0',
|
version: '>=22.18.0',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'n/prefer-global/buffer': ['error', 'never'],
|
'n/prefer-global/buffer': ['error', 'never'],
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ interface PluginOptions {
|
||||||
root: string;
|
root: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLOBAL_CONFIG_FILE_NAME = '_app.config.js';
|
const GLOBAL_CONFIG_FILE_NAME = '_app-config';
|
||||||
const VBEN_ADMIN_PRO_APP_CONF = '_VBEN_ADMIN_PRO_APP_CONF_';
|
const VBEN_ADMIN_PRO_APP_CONF = '_VBEN_ADMIN_PRO_APP_CONF_';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -27,6 +27,7 @@ async function viteExtraAppConfigPlugin({
|
||||||
}: PluginOptions): Promise<PluginOption | undefined> {
|
}: PluginOptions): Promise<PluginOption | undefined> {
|
||||||
let publicPath: string;
|
let publicPath: string;
|
||||||
let source: string;
|
let source: string;
|
||||||
|
let hash: string;
|
||||||
|
|
||||||
if (!isBuild) {
|
if (!isBuild) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -38,11 +39,12 @@ async function viteExtraAppConfigPlugin({
|
||||||
async configResolved(config) {
|
async configResolved(config) {
|
||||||
publicPath = ensureTrailingSlash(config.base);
|
publicPath = ensureTrailingSlash(config.base);
|
||||||
source = await getConfigSource();
|
source = await getConfigSource();
|
||||||
|
hash = generatorContentHash(source, 8);
|
||||||
},
|
},
|
||||||
async generateBundle() {
|
async generateBundle() {
|
||||||
try {
|
try {
|
||||||
this.emitFile({
|
this.emitFile({
|
||||||
fileName: GLOBAL_CONFIG_FILE_NAME,
|
fileName: `${GLOBAL_CONFIG_FILE_NAME}-${version}-${hash}.js`,
|
||||||
source,
|
source,
|
||||||
type: 'asset',
|
type: 'asset',
|
||||||
});
|
});
|
||||||
|
|
@ -58,9 +60,7 @@ async function viteExtraAppConfigPlugin({
|
||||||
},
|
},
|
||||||
name: 'vite:extra-app-config',
|
name: 'vite:extra-app-config',
|
||||||
async transformIndexHtml(html) {
|
async transformIndexHtml(html) {
|
||||||
const hash = `v=${version}-${generatorContentHash(source, 8)}`;
|
const appConfigSrc = `${publicPath}${GLOBAL_CONFIG_FILE_NAME}-${version}-${hash}.js`;
|
||||||
|
|
||||||
const appConfigSrc = `${publicPath}${GLOBAL_CONFIG_FILE_NAME}?${hash}`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html,
|
html,
|
||||||
|
|
|
||||||
|
|
@ -105,5 +105,5 @@
|
||||||
"node": "^22.18.0 || ^24.0.0",
|
"node": "^22.18.0 || ^24.0.0",
|
||||||
"pnpm": ">=11.0.0"
|
"pnpm": ">=11.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.2.2"
|
"packageManager": "pnpm@11.5.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/vue": "catalog:",
|
"@iconify/vue": "catalog:",
|
||||||
"lucide-vue-next": "catalog:",
|
"@lucide/vue": "catalog:",
|
||||||
"vue": "catalog:"
|
"vue": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ export {
|
||||||
EyeOff,
|
EyeOff,
|
||||||
FoldHorizontal,
|
FoldHorizontal,
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
Github,
|
|
||||||
Grid,
|
Grid,
|
||||||
Grip,
|
Grip,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
|
|
@ -99,4 +98,4 @@ export {
|
||||||
Upload,
|
Upload,
|
||||||
UserRoundPen,
|
UserRoundPen,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-vue-next';
|
} from '@lucide/vue';
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,21 @@ it('updateCSSVariables should update CSS variables in :root selector', () => {
|
||||||
updatedStyleContent?.includes('fontSize: 16px;'),
|
updatedStyleContent?.includes('fontSize: 16px;'),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('updateCSSVariables should support a custom selector', () => {
|
||||||
|
document.head.innerHTML = `<style id="tdesign-styles"></style>`;
|
||||||
|
|
||||||
|
// 使用自定义选择器(如 TDesign 的 theme-mode 选择器)更新 CSS 变量
|
||||||
|
updateCSSVariables(
|
||||||
|
{ '--td-brand-color': 'rgb(0, 82, 217)' },
|
||||||
|
'tdesign-styles',
|
||||||
|
":root[theme-mode='dark']",
|
||||||
|
);
|
||||||
|
|
||||||
|
const styleElement = document.querySelector('#tdesign-styles');
|
||||||
|
const content = styleElement?.textContent ?? '';
|
||||||
|
|
||||||
|
// 选择器与变量都应正确写入
|
||||||
|
expect(content.startsWith(":root[theme-mode='dark'] {")).toBe(true);
|
||||||
|
expect(content.includes('--td-brand-color: rgb(0, 82, 217);')).toBe(true);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
/**
|
/**
|
||||||
* 更新 CSS 变量的函数
|
* 更新 CSS 变量的函数
|
||||||
* @param variables 要更新的 CSS 变量与其新值的映射
|
* @param variables 要更新的 CSS 变量与其新值的映射
|
||||||
|
* @param id 内联样式表的 id,便于复用与覆盖
|
||||||
|
* @param selector CSS 变量挂载的选择器,默认 `:root`。
|
||||||
|
* 对于像 TDesign 这种将变量定义在 `:root[theme-mode='dark']` 等更高优先级选择器下的组件库,
|
||||||
|
* 需要传入相同(或更高)优先级的选择器才能正确覆盖。
|
||||||
*/
|
*/
|
||||||
function updateCSSVariables(
|
function updateCSSVariables(
|
||||||
variables: { [key: string]: string },
|
variables: { [key: string]: string },
|
||||||
id = '__vben-styles__',
|
id = '__vben-styles__',
|
||||||
|
selector = ':root',
|
||||||
): void {
|
): void {
|
||||||
// 获取或创建内联样式表元素
|
// 获取或创建内联样式表元素
|
||||||
const styleElement =
|
const styleElement =
|
||||||
|
|
@ -13,7 +18,7 @@ function updateCSSVariables(
|
||||||
styleElement.id = id;
|
styleElement.id = id;
|
||||||
|
|
||||||
// 构建要更新的 CSS 变量的样式文本
|
// 构建要更新的 CSS 变量的样式文本
|
||||||
let cssText = ':root {';
|
let cssText = `${selector} {`;
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
if (Object.prototype.hasOwnProperty.call(variables, key)) {
|
if (Object.prototype.hasOwnProperty.call(variables, key)) {
|
||||||
cssText += `${key}: ${variables[key]};`;
|
cssText += `${key}: ${variables[key]};`;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const messages: Record<Locale, Record<string, string>> = {
|
||||||
prompt: 'Prompt',
|
prompt: 'Prompt',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
submit: 'Submit',
|
submit: 'Submit',
|
||||||
|
confirmTitle: 'Please Confirm',
|
||||||
},
|
},
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
|
|
@ -18,6 +19,7 @@ export const messages: Record<Locale, Record<string, string>> = {
|
||||||
prompt: '提示',
|
prompt: '提示',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
|
confirmTitle: '请确认',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormSchema } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { Plus, X } from '@vben-core/icons';
|
||||||
|
import {
|
||||||
|
VbenButton,
|
||||||
|
VbenIconButton,
|
||||||
|
VbenRenderContent,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { useFieldArray } from 'vee-validate';
|
||||||
|
|
||||||
|
import FormField from '../form-render/form-field.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'VbenFormFieldArray', inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 操作列表头文案 */
|
||||||
|
actionText?: string;
|
||||||
|
/** 「添加」按钮文案 */
|
||||||
|
addButtonText?: string;
|
||||||
|
/**
|
||||||
|
* 新增一行时生成的默认数据;缺省时按 schema 的 fieldName 生成空对象
|
||||||
|
*/
|
||||||
|
createRow?: () => Record<string, any>;
|
||||||
|
disabled?: boolean;
|
||||||
|
/** 空数据文案 */
|
||||||
|
emptyText?: string;
|
||||||
|
/** 最多行数 */
|
||||||
|
max?: number;
|
||||||
|
/** 最少行数 */
|
||||||
|
min?: number;
|
||||||
|
/**
|
||||||
|
* 字段路径,由外层 FormField 通过 componentField 透传(vee-validate 的 name)
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* 列定义,每一列就是一个子字段(复用 FormSchema)
|
||||||
|
*/
|
||||||
|
schema?: FormSchema[];
|
||||||
|
/** 是否显示序号列 */
|
||||||
|
showIndex?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
actionText: '操作',
|
||||||
|
addButtonText: '添加一行',
|
||||||
|
createRow: undefined,
|
||||||
|
disabled: false,
|
||||||
|
emptyText: '暂无数据',
|
||||||
|
max: Number.POSITIVE_INFINITY,
|
||||||
|
min: 0,
|
||||||
|
name: '',
|
||||||
|
schema: () => [],
|
||||||
|
showIndex: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const arrayPath = computed(() => props.name);
|
||||||
|
|
||||||
|
const { fields, push, remove } = useFieldArray<Record<string, any>>(
|
||||||
|
() => arrayPath.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const canAdd = computed(() => fields.value.length < props.max);
|
||||||
|
const canRemove = computed(() => fields.value.length > props.min);
|
||||||
|
|
||||||
|
function buildDefaultRow(): Record<string, any> {
|
||||||
|
if (props.createRow) {
|
||||||
|
return props.createRow();
|
||||||
|
}
|
||||||
|
return Object.fromEntries(props.schema.map((col) => [col.fieldName, null]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRow() {
|
||||||
|
if (props.disabled || !canAdd.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
push(buildDefaultRow());
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(index: number) {
|
||||||
|
if (props.disabled || !canRemove.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把列定义转换为子单元格 FormField 所需的 props。
|
||||||
|
* - fieldName 替换为嵌套路径 `name[index].fieldName`,让校验与取值落在数组元素上
|
||||||
|
* - hideLabel:表头已展示列名,单元格不重复显示
|
||||||
|
*/
|
||||||
|
function cellProps(col: FormSchema, index: number) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
commonComponentProps: {},
|
||||||
|
disabled: props.disabled,
|
||||||
|
fieldName: `${arrayPath.value}[${index}].${col.fieldName}`,
|
||||||
|
formFieldProps: {},
|
||||||
|
hideLabel: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('w-full', $attrs.class as string)">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-border border-b">
|
||||||
|
<th
|
||||||
|
v-if="showIndex"
|
||||||
|
class="text-muted-foreground w-12 px-2 py-2 text-left text-sm font-normal"
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="col in schema"
|
||||||
|
:key="col.fieldName"
|
||||||
|
class="text-muted-foreground px-2 py-2 text-left text-sm font-normal"
|
||||||
|
>
|
||||||
|
<VbenRenderContent :content="col.label" />
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="text-muted-foreground w-16 px-2 py-2 text-left text-sm font-normal"
|
||||||
|
>
|
||||||
|
{{ actionText }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(entry, index) in fields"
|
||||||
|
:key="entry.key"
|
||||||
|
class="border-border/60 border-b align-top"
|
||||||
|
>
|
||||||
|
<td v-if="showIndex" class="text-muted-foreground px-2 py-3 text-sm">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</td>
|
||||||
|
<td v-for="col in schema" :key="col.fieldName" class="px-2 py-2">
|
||||||
|
<FormField v-bind="cellProps(col, index)" />
|
||||||
|
</td>
|
||||||
|
<td class="px-2 py-3">
|
||||||
|
<VbenIconButton
|
||||||
|
:disabled="disabled || !canRemove"
|
||||||
|
:on-click="() => removeRow(index)"
|
||||||
|
class="text-muted-foreground hover:text-destructive"
|
||||||
|
>
|
||||||
|
<X class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="fields.length === 0"
|
||||||
|
class="text-muted-foreground border-border/60 border-b py-6 text-center text-sm"
|
||||||
|
>
|
||||||
|
{{ emptyText }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VbenButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
:disabled="disabled || !canAdd"
|
||||||
|
class="mt-3 w-full border-dashed"
|
||||||
|
@click="addRow"
|
||||||
|
>
|
||||||
|
<Plus class="mr-1 size-4" />
|
||||||
|
{{ addButtonText }}
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -20,6 +20,8 @@ import { globalShareState } from '@vben-core/shared/global-state';
|
||||||
|
|
||||||
import { defineRule } from 'vee-validate';
|
import { defineRule } from 'vee-validate';
|
||||||
|
|
||||||
|
import VbenFormFieldArray from './components/form-field-array.vue';
|
||||||
|
|
||||||
const DEFAULT_MODEL_PROP_NAME = 'modelValue';
|
const DEFAULT_MODEL_PROP_NAME = 'modelValue';
|
||||||
|
|
||||||
export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};
|
export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};
|
||||||
|
|
@ -28,6 +30,7 @@ export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||||
DefaultButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
|
DefaultButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
|
||||||
PrimaryButton: h(VbenButton, { size: 'sm', variant: 'default' }),
|
PrimaryButton: h(VbenButton, { size: 'sm', variant: 'default' }),
|
||||||
VbenCheckbox,
|
VbenCheckbox,
|
||||||
|
VbenFormFieldArray,
|
||||||
VbenInput,
|
VbenInput,
|
||||||
VbenInputPassword,
|
VbenInputPassword,
|
||||||
VbenPinInput,
|
VbenPinInput,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export type {
|
||||||
BaseFormComponentType,
|
BaseFormComponentType,
|
||||||
ExtendedFormApi,
|
ExtendedFormApi,
|
||||||
FormLayout,
|
FormLayout,
|
||||||
|
VbenFormFieldArrayProps,
|
||||||
VbenFormProps,
|
VbenFormProps,
|
||||||
FormSchema as VbenFormSchema,
|
FormSchema as VbenFormSchema,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export type BaseFormComponentType =
|
||||||
| 'DefaultButton'
|
| 'DefaultButton'
|
||||||
| 'PrimaryButton'
|
| 'PrimaryButton'
|
||||||
| 'VbenCheckbox'
|
| 'VbenCheckbox'
|
||||||
|
| 'VbenFormFieldArray'
|
||||||
| 'VbenInput'
|
| 'VbenInput'
|
||||||
| 'VbenInputPassword'
|
| 'VbenInputPassword'
|
||||||
| 'VbenPinInput'
|
| 'VbenPinInput'
|
||||||
|
|
@ -309,6 +310,32 @@ export type FormSchema<
|
||||||
P extends Record<string, any> = Record<never, never>,
|
P extends Record<string, any> = Record<never, never>,
|
||||||
> = FormSchemaDiscriminated<T, P> | FormSchemaFallback<T>;
|
> = FormSchemaDiscriminated<T, P> | FormSchemaFallback<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组编辑器(VbenFormFieldArray)的组件参数
|
||||||
|
*/
|
||||||
|
export interface VbenFormFieldArrayProps<
|
||||||
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
P extends Record<string, any> = Record<never, never>,
|
||||||
|
> {
|
||||||
|
/** 操作列表头文案 */
|
||||||
|
actionText?: string;
|
||||||
|
/** 「添加」按钮文案 */
|
||||||
|
addButtonText?: string;
|
||||||
|
/** 新增一行时生成的默认数据;缺省时按列定义的 fieldName 生成空对象 */
|
||||||
|
createRow?: () => Record<string, any>;
|
||||||
|
disabled?: boolean;
|
||||||
|
/** 空数据文案 */
|
||||||
|
emptyText?: string;
|
||||||
|
/** 最多行数 */
|
||||||
|
max?: number;
|
||||||
|
/** 最少行数 */
|
||||||
|
min?: number;
|
||||||
|
/** 列定义,每一列是一个子字段(复用 FormSchema) */
|
||||||
|
schema: FormSchema<T, P>[];
|
||||||
|
/** 是否显示序号列 */
|
||||||
|
showIndex?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type HandleSubmitFn = (
|
export type HandleSubmitFn = (
|
||||||
values: Record<string, any>,
|
values: Record<string, any>,
|
||||||
) => Promise<void> | void;
|
) => Promise<void> | void;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import type {
|
||||||
|
|
||||||
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
|
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
|
||||||
|
|
||||||
import { useStore } from '@vben-core/shared/store';
|
import { useSelector } from '@vben-core/shared/store';
|
||||||
|
|
||||||
import { FormApi } from './form-api';
|
import { FormApi } from './form-api';
|
||||||
import VbenUseForm from './vben-use-form.vue';
|
import VbenUseForm from './vben-use-form.vue';
|
||||||
|
|
@ -19,7 +19,7 @@ export function useVbenForm<
|
||||||
const api = new FormApi(options as unknown as VbenFormProps);
|
const api = new FormApi(options as unknown as VbenFormProps);
|
||||||
const extendedApi: ExtendedFormApi = api as never;
|
const extendedApi: ExtendedFormApi = api as never;
|
||||||
extendedApi.useStore = (selector) => {
|
extendedApi.useStore = (selector) => {
|
||||||
return useStore(api.store, selector);
|
return useSelector(api.store, selector);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Form = defineComponent(
|
const Form = defineComponent(
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const menuIcon = computed(() =>
|
||||||
const isHttp = computed(() => isHttpUrl(item.parentPaths.at(-1)));
|
const isHttp = computed(() => isHttpUrl(item.parentPaths.at(-1)));
|
||||||
|
|
||||||
const isTopLevelMenuItem = computed(
|
const isTopLevelMenuItem = computed(
|
||||||
() => parentMenu.value?.type.name === 'Menu',
|
() => parentMenu.value?.type.name === 'MenuUI',
|
||||||
);
|
);
|
||||||
|
|
||||||
const collapseShowTitle = computed(
|
const collapseShowTitle = computed(
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import SubMenu from './sub-menu.vue';
|
||||||
|
|
||||||
interface Props extends MenuProps {}
|
interface Props extends MenuProps {}
|
||||||
|
|
||||||
defineOptions({ name: 'Menu' });
|
defineOptions({ name: 'MenuUI' });
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
accordion: true,
|
accordion: true,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const opened = computed(() => {
|
||||||
return rootMenu?.openedMenus.includes(props.path);
|
return rootMenu?.openedMenus.includes(props.path);
|
||||||
});
|
});
|
||||||
const isTopLevelMenuSubmenu = computed(
|
const isTopLevelMenuSubmenu = computed(
|
||||||
() => parentMenu.value?.type.name === 'Menu',
|
() => parentMenu.value?.type.name === 'MenuUI',
|
||||||
);
|
);
|
||||||
const mode = computed(() => rootMenu?.props.mode ?? 'vertical');
|
const mode = computed(() => rootMenu?.props.mode ?? 'vertical');
|
||||||
const rounded = computed(() => rootMenu?.props.rounded);
|
const rounded = computed(() => rootMenu?.props.rounded);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ function useSubMenuContext() {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
throw new Error('instance is required');
|
throw new Error('instance is required');
|
||||||
}
|
}
|
||||||
const parentMenu = findComponentUpward(instance, ['Menu', 'SubMenu']);
|
const parentMenu = findComponentUpward(instance, ['MenuUI', 'SubMenu']);
|
||||||
const subMenu = inject(`subMenu:${parentMenu?.uid}`) as SubMenuProvider;
|
const subMenu = inject(`subMenu:${parentMenu?.uid}`) as SubMenuProvider;
|
||||||
return subMenu;
|
return subMenu;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ function useMenu() {
|
||||||
const parentPaths = computed(() => {
|
const parentPaths = computed(() => {
|
||||||
let parent = instance.parent;
|
let parent = instance.parent;
|
||||||
const paths: string[] = [instance.props.path as string];
|
const paths: string[] = [instance.props.path as string];
|
||||||
while (parent?.type.name !== 'Menu') {
|
while (parent?.type.name !== 'MenuUI') {
|
||||||
if (parent?.props.path) {
|
if (parent?.props.path) {
|
||||||
paths.unshift(parent.props.path as string);
|
paths.unshift(parent.props.path as string);
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ function useMenu() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const parentMenu = computed(() => {
|
const parentMenu = computed(() => {
|
||||||
return findComponentUpward(instance, ['Menu', 'SubMenu']);
|
return findComponentUpward(instance, ['MenuUI', 'SubMenu']);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ async function handleOpenChange(val: boolean) {
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
containerClass,
|
containerClass,
|
||||||
'inset-x-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-130 sm:max-w-[80%] sm:rounded-(--radius)',
|
'flex max-h-[80%] flex-col p-0 duration-300 sm:w-130 sm:max-w-[80%] sm:rounded-(--radius)',
|
||||||
{
|
{
|
||||||
'border border-border': bordered,
|
'border border-border': bordered,
|
||||||
'shadow-3xl': !bordered,
|
'shadow-3xl': !bordered,
|
||||||
|
|
@ -197,7 +197,7 @@ async function handleOpenChange(val: boolean) {
|
||||||
<component
|
<component
|
||||||
:is="components.DefaultButton || VbenButton"
|
:is="components.DefaultButton || VbenButton"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
@click="handleCancel"
|
@click="handleCancel"
|
||||||
>
|
>
|
||||||
{{ cancelText || $t('cancel') }}
|
{{ cancelText || $t('cancel') }}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ const components = globalShareState.getComponents();
|
||||||
const id = useId();
|
const id = useId();
|
||||||
provide('DISMISSABLE_DRAWER_ID', id);
|
provide('DISMISSABLE_DRAWER_ID', id);
|
||||||
|
|
||||||
// const wrapperRef = ref<HTMLElement>();
|
// @ts-expect-error unused
|
||||||
|
const wrapperRef = ref<HTMLElement>();
|
||||||
const { $t } = useSimpleLocale();
|
const { $t } = useSimpleLocale();
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
|
|
||||||
|
|
@ -285,8 +286,8 @@ const getForceMount = computed(() => {
|
||||||
<SheetDescription />
|
<SheetDescription />
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
</template>
|
</template>
|
||||||
<!-- 注释掉的部分 <div ref="wrapperRef" -->
|
|
||||||
<div
|
<div
|
||||||
|
ref="wrapperRef"
|
||||||
:class="
|
:class="
|
||||||
cn('relative flex-1 overflow-y-auto p-3', contentClass, {
|
cn('relative flex-1 overflow-y-auto p-3', contentClass, {
|
||||||
'pointer-events-none': showLoading || submitting,
|
'pointer-events-none': showLoading || submitting,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import { usePreferences } from '@vben-core/preferences';
|
import { usePreferences } from '@vben-core/preferences';
|
||||||
import { useStore } from '@vben-core/shared/store';
|
import { useSelector } from '@vben-core/shared/store';
|
||||||
|
|
||||||
import { DrawerApi } from './drawer-api';
|
import { DrawerApi } from './drawer-api';
|
||||||
import VbenDrawer from './drawer.vue';
|
import VbenDrawer from './drawer.vue';
|
||||||
|
|
@ -55,7 +55,7 @@ export function useVbenDrawer<
|
||||||
// 不能用 Object.assign,会丢失 api 的原型函数
|
// 不能用 Object.assign,会丢失 api 的原型函数
|
||||||
Object.setPrototypeOf(extendedApi, api);
|
Object.setPrototypeOf(extendedApi, api);
|
||||||
},
|
},
|
||||||
defaultOptions,
|
options: defaultOptions,
|
||||||
async reCreateDrawer() {
|
async reCreateDrawer() {
|
||||||
isDrawerReady.value = false;
|
isDrawerReady.value = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
@ -109,7 +109,7 @@ export function useVbenDrawer<
|
||||||
const extendedApi: ExtendedDrawerApi = api as never;
|
const extendedApi: ExtendedDrawerApi = api as never;
|
||||||
|
|
||||||
extendedApi.useStore = (selector) => {
|
extendedApi.useStore = (selector) => {
|
||||||
return useStore(api.store, selector);
|
return useSelector(api.store, selector);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Drawer = defineComponent(
|
const Drawer = defineComponent(
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,9 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ExtendedModalApi, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalProps } from './modal';
|
||||||
|
|
||||||
import {
|
import { computed, nextTick, onDeactivated, ref, unref, watch } from 'vue';
|
||||||
computed,
|
|
||||||
nextTick,
|
|
||||||
onDeactivated,
|
|
||||||
provide,
|
|
||||||
ref,
|
|
||||||
unref,
|
|
||||||
useId,
|
|
||||||
watch,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import {
|
import { usePriorityValues, useSimpleLocale } from '@vben-core/composables';
|
||||||
useIsMobile,
|
|
||||||
usePriorityValues,
|
|
||||||
useSimpleLocale,
|
|
||||||
} from '@vben-core/composables';
|
|
||||||
import { Expand, Shrink } from '@vben-core/icons';
|
import { Expand, Shrink } from '@vben-core/icons';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -57,12 +44,7 @@ const headerRef = ref();
|
||||||
// @ts-expect-error unused
|
// @ts-expect-error unused
|
||||||
const footerRef = ref();
|
const footerRef = ref();
|
||||||
|
|
||||||
const id = useId();
|
|
||||||
|
|
||||||
provide('DISMISSABLE_MODAL_ID', id);
|
|
||||||
|
|
||||||
const { $t } = useSimpleLocale();
|
const { $t } = useSimpleLocale();
|
||||||
const { isMobile } = useIsMobile();
|
|
||||||
const state = props.modalApi?.useStore?.();
|
const state = props.modalApi?.useStore?.();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -101,7 +83,7 @@ const {
|
||||||
zIndex,
|
zIndex,
|
||||||
} = usePriorityValues(props, state);
|
} = usePriorityValues(props, state);
|
||||||
|
|
||||||
const shouldFullscreen = computed(() => fullscreen.value || isMobile.value);
|
const shouldFullscreen = computed(() => fullscreen.value);
|
||||||
|
|
||||||
const shouldDraggable = computed(
|
const shouldDraggable = computed(
|
||||||
() => draggable.value && !shouldFullscreen.value && header.value,
|
() => draggable.value && !shouldFullscreen.value && header.value,
|
||||||
|
|
@ -199,15 +181,8 @@ function handleOpenAutoFocus(e: Event) {
|
||||||
|
|
||||||
// pointer-down-outside
|
// pointer-down-outside
|
||||||
function pointerDownOutside(e: Event) {
|
function pointerDownOutside(e: Event) {
|
||||||
const target = e.target as HTMLElement;
|
if (!closeOnClickModal.value || submitting.value) {
|
||||||
const isDismissableModal = target?.dataset.dismissableModal;
|
|
||||||
if (
|
|
||||||
!closeOnClickModal.value ||
|
|
||||||
isDismissableModal !== id ||
|
|
||||||
submitting.value
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,6 +191,10 @@ function handleFocusOutside(e: Event) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCloseAutoFocus(_e: Event) {
|
||||||
|
// allow reka-ui to return focus to the trigger element on close
|
||||||
|
}
|
||||||
|
|
||||||
const getForceMount = computed(() => {
|
const getForceMount = computed(() => {
|
||||||
return !unref(destroyOnClose) && unref(firstOpened);
|
return !unref(destroyOnClose) && unref(firstOpened);
|
||||||
});
|
});
|
||||||
|
|
@ -233,7 +212,7 @@ function handleClosed() {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
:modal="false"
|
:modal="modal"
|
||||||
:open="state?.isOpen"
|
:open="state?.isOpen"
|
||||||
@update:open="() => (!submitting ? modalApi?.close() : undefined)"
|
@update:open="() => (!submitting ? modalApi?.close() : undefined)"
|
||||||
>
|
>
|
||||||
|
|
@ -242,13 +221,15 @@ function handleClosed() {
|
||||||
:append-to="getAppendTo"
|
:append-to="getAppendTo"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'inset-x-0 top-[10vh] mx-auto flex max-h-[80%] w-130 flex-col p-0',
|
'inset-x-0 top-[10vh] mx-auto flex w-130 flex-col p-0',
|
||||||
shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-(--radius)',
|
shouldFullscreen ? 'rounded-none' : 'rounded-(--radius)',
|
||||||
modalClass,
|
modalClass,
|
||||||
{
|
{
|
||||||
'border border-border': bordered,
|
'border border-border': bordered,
|
||||||
'shadow-3xl': !bordered,
|
'shadow-3xl': !bordered,
|
||||||
'top-0 left-0 size-full max-h-full transform-[translate(0,0)]!':
|
'max-h-[min(80%,calc(100dvh-20px))] max-w-[calc(100vw-20px)]':
|
||||||
|
!shouldFullscreen,
|
||||||
|
'top-0 left-0 size-full max-h-full max-w-full transform-[translate(0,0)]!':
|
||||||
shouldFullscreen,
|
shouldFullscreen,
|
||||||
'top-1/2': centered && !shouldFullscreen,
|
'top-1/2': centered && !shouldFullscreen,
|
||||||
'duration-300': !dragging,
|
'duration-300': !dragging,
|
||||||
|
|
@ -264,7 +245,7 @@ function handleClosed() {
|
||||||
:z-index="zIndex"
|
:z-index="zIndex"
|
||||||
:overlay-blur="overlayBlur"
|
:overlay-blur="overlayBlur"
|
||||||
close-class="top-3"
|
close-class="top-3"
|
||||||
@close-auto-focus="handleFocusOutside"
|
@close-auto-focus="handleCloseAutoFocus"
|
||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
:close-disabled="submitting"
|
:close-disabled="submitting"
|
||||||
@escape-key-down="escapeKeyDown"
|
@escape-key-down="escapeKeyDown"
|
||||||
|
|
@ -322,7 +303,7 @@ function handleClosed() {
|
||||||
<VbenLoading v-if="showLoading || submitting" spinning />
|
<VbenLoading v-if="showLoading || submitting" spinning />
|
||||||
<VbenIconButton
|
<VbenIconButton
|
||||||
v-if="fullscreenButton"
|
v-if="fullscreenButton"
|
||||||
class="absolute top-3 right-10 flex-center hidden size-6 rounded-full px-1 text-lg text-foreground/80 opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-hidden disabled:pointer-events-none sm:block"
|
class="absolute top-3 right-10 flex-center size-6 rounded-full px-1 text-lg text-foreground/80 opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-hidden disabled:pointer-events-none"
|
||||||
@click="handleFullscreen"
|
@click="handleFullscreen"
|
||||||
>
|
>
|
||||||
<Shrink v-if="fullscreen" class="size-3.5" />
|
<Shrink v-if="fullscreen" class="size-3.5" />
|
||||||
|
|
@ -347,7 +328,7 @@ function handleClosed() {
|
||||||
<component
|
<component
|
||||||
:is="components.DefaultButton || VbenButton"
|
:is="components.DefaultButton || VbenButton"
|
||||||
v-if="showCancelButton"
|
v-if="showCancelButton"
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
@click="() => modalApi?.onCancel()"
|
@click="() => modalApi?.onCancel()"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import { usePreferences } from '@vben-core/preferences';
|
import { usePreferences } from '@vben-core/preferences';
|
||||||
import { useStore } from '@vben-core/shared/store';
|
import { useSelector } from '@vben-core/shared/store';
|
||||||
|
|
||||||
import { ModalApi } from './modal-api';
|
import { ModalApi } from './modal-api';
|
||||||
import VbenModal from './modal.vue';
|
import VbenModal from './modal.vue';
|
||||||
|
|
@ -51,7 +51,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||||
Object.setPrototypeOf(extendedApi, api);
|
Object.setPrototypeOf(extendedApi, api);
|
||||||
},
|
},
|
||||||
consumed: false,
|
consumed: false,
|
||||||
defaultOptions,
|
options: defaultOptions,
|
||||||
async reCreateModal() {
|
async reCreateModal() {
|
||||||
isModalReady.value = false;
|
isModalReady.value = false;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
@ -116,7 +116,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||||
const extendedApi: ExtendedModalApi = api as never;
|
const extendedApi: ExtendedModalApi = api as never;
|
||||||
|
|
||||||
extendedApi.useStore = (selector) => {
|
extendedApi.useStore = (selector) => {
|
||||||
return useStore(api.store, selector);
|
return useSelector(api.store, selector);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Modal = defineComponent(
|
const Modal = defineComponent(
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@lucide/vue": "catalog:",
|
||||||
"@vben-core/composables": "workspace:*",
|
"@vben-core/composables": "workspace:*",
|
||||||
"@vben-core/design": "workspace:*",
|
"@vben-core/design": "workspace:*",
|
||||||
"@vben-core/icons": "workspace:*",
|
"@vben-core/icons": "workspace:*",
|
||||||
|
|
@ -44,7 +45,6 @@
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"@vueuse/core": "catalog:",
|
"@vueuse/core": "catalog:",
|
||||||
"class-variance-authority": "catalog:",
|
"class-variance-authority": "catalog:",
|
||||||
"lucide-vue-next": "catalog:",
|
|
||||||
"reka-ui": "catalog:",
|
"reka-ui": "catalog:",
|
||||||
"vee-validate": "catalog:",
|
"vee-validate": "catalog:",
|
||||||
"vue": "catalog:"
|
"vue": "catalog:"
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const { handleClick, visible } = useBackTop(props);
|
||||||
:style="backTopStyle"
|
:style="backTopStyle"
|
||||||
class="data z-popup bg-background shadow-float hover:bg-heavy dark:bg-accent dark:hover:bg-heavy fixed bottom-10 size-10 rounded-full duration-500"
|
class="data z-popup bg-background shadow-float hover:bg-heavy dark:bg-accent dark:hover:bg-heavy fixed bottom-10 size-10 rounded-full duration-500"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="icon"
|
variant="ghost"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<ArrowUpToLine class="size-4" />
|
<ArrowUpToLine class="size-4" />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { AsTag } from 'reka-ui';
|
||||||
|
|
||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
import type { ButtonVariants, ButtonVariantSize } from '../../ui';
|
import type { ButtonVariants } from '../../ui';
|
||||||
|
|
||||||
export interface VbenButtonProps {
|
export interface VbenButtonProps {
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,8 +19,8 @@ export interface VbenButtonProps {
|
||||||
class?: any;
|
class?: any;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
size?: ButtonVariantSize;
|
size?: ButtonVariants['size'];
|
||||||
variant?: ButtonVariants;
|
variant?: ButtonVariants['variant'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomRenderType = (() => Component | string) | string;
|
export type CustomRenderType = (() => Component | string) | string;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ interface Props extends VbenButtonProps {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
tooltipDelayDuration?: number;
|
tooltipDelayDuration?: number;
|
||||||
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
||||||
variant?: ButtonVariants;
|
variant?: ButtonVariants['variant'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -24,7 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
tooltipDelayDuration: 200,
|
tooltipDelayDuration: 200,
|
||||||
tooltipSide: 'bottom',
|
tooltipSide: 'bottom',
|
||||||
variant: 'icon',
|
variant: 'ghost',
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
import { useNamespace } from '@vben-core/composables';
|
import { useNamespace } from '@vben-core/composables';
|
||||||
|
|
||||||
import { ChevronsDown } from 'lucide-vue-next';
|
import { ChevronsDown } from '@lucide/vue';
|
||||||
import {
|
import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleRoot,
|
CollapsibleRoot,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type { ClassType } from '@vben-core/typings';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { ChevronsDown } from 'lucide-vue-next';
|
import { ChevronsDown } from '@lucide/vue';
|
||||||
import {
|
import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleRoot,
|
CollapsibleRoot,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
import type { DescriptionsRenderNode, DescriptionsSize } from './types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { VbenRenderContent } from '../render-content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 是否边框模式 */
|
||||||
|
bordered?: boolean;
|
||||||
|
/** 是否显示冒号(仅非边框模式生效) */
|
||||||
|
colon?: boolean;
|
||||||
|
/** 内容 */
|
||||||
|
content?: DescriptionsRenderNode | null;
|
||||||
|
/** 内容样式 */
|
||||||
|
contentStyle?: CSSProperties;
|
||||||
|
/** 单项自定义类名 */
|
||||||
|
itemClass?: string;
|
||||||
|
/** 标签 */
|
||||||
|
label?: DescriptionsRenderNode | null;
|
||||||
|
/** 标签样式 */
|
||||||
|
labelStyle?: CSSProperties;
|
||||||
|
/** 尺寸 */
|
||||||
|
size?: DescriptionsSize;
|
||||||
|
/** 跨列数 */
|
||||||
|
span?: number;
|
||||||
|
/** 渲染标签 th 还是 td */
|
||||||
|
tag: 'td' | 'th';
|
||||||
|
/** 单元格类型 */
|
||||||
|
type: 'content' | 'item' | 'label';
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
bordered: false,
|
||||||
|
colon: true,
|
||||||
|
content: null,
|
||||||
|
contentStyle: undefined,
|
||||||
|
itemClass: undefined,
|
||||||
|
label: null,
|
||||||
|
labelStyle: undefined,
|
||||||
|
size: 'middle',
|
||||||
|
span: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const BORDERED_PADDING: Record<DescriptionsSize, string> = {
|
||||||
|
large: 'px-6 py-4',
|
||||||
|
middle: 'px-4 py-2.5',
|
||||||
|
small: 'px-3 py-2',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAIN_PADDING: Record<DescriptionsSize, string> = {
|
||||||
|
large: 'pb-6',
|
||||||
|
middle: 'pb-4',
|
||||||
|
small: 'pb-2',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 冒号通过伪元素追加,避免标签为渲染函数时无法拼接
|
||||||
|
const COLON_CLASS = "after:content-[':']";
|
||||||
|
|
||||||
|
const hasLabel = computed(
|
||||||
|
() => props.label !== null && props.label !== undefined,
|
||||||
|
);
|
||||||
|
const hasContent = computed(
|
||||||
|
() => props.content !== null && props.content !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 数字 0 会被 VbenRenderContent 当作 falsy 隐藏,这里转为字符串保证展示;
|
||||||
|
// 同时将 null 归一为 undefined,匹配 VbenRenderContent 的 content 类型
|
||||||
|
const displayLabel = computed(() => {
|
||||||
|
if (props.label === null || props.label === undefined) return undefined;
|
||||||
|
return typeof props.label === 'number' ? String(props.label) : props.label;
|
||||||
|
});
|
||||||
|
const displayContent = computed(() => {
|
||||||
|
if (props.content === null || props.content === undefined) return undefined;
|
||||||
|
return typeof props.content === 'number'
|
||||||
|
? String(props.content)
|
||||||
|
: props.content;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cellClass = computed(() => {
|
||||||
|
if (props.bordered) {
|
||||||
|
return cn(
|
||||||
|
'border border-border align-top break-words',
|
||||||
|
BORDERED_PADDING[props.size],
|
||||||
|
props.type === 'label'
|
||||||
|
? 'bg-muted/50 text-start font-normal text-foreground'
|
||||||
|
: 'text-foreground',
|
||||||
|
props.itemClass,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cn('align-top', PLAIN_PADDING[props.size], props.itemClass);
|
||||||
|
});
|
||||||
|
|
||||||
|
const labelClass = computed(() =>
|
||||||
|
cn('mr-2 shrink-0 text-muted-foreground', props.colon && COLON_CLASS),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="tag" :class="cellClass" :colspan="span">
|
||||||
|
<!-- 边框模式:每个单元格仅承载 label 或 content -->
|
||||||
|
<template v-if="bordered">
|
||||||
|
<span v-if="hasLabel" :style="labelStyle">
|
||||||
|
<VbenRenderContent :content="displayLabel" />
|
||||||
|
</span>
|
||||||
|
<span v-if="hasContent" :style="contentStyle">
|
||||||
|
<VbenRenderContent :content="displayContent" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 非边框模式:label + content 容器 -->
|
||||||
|
<div v-else class="flex">
|
||||||
|
<span v-if="hasLabel" :class="labelClass" :style="labelStyle">
|
||||||
|
<VbenRenderContent :content="displayLabel" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="hasContent"
|
||||||
|
class="break-words text-foreground"
|
||||||
|
:style="contentStyle"
|
||||||
|
>
|
||||||
|
<VbenRenderContent :content="displayContent" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import type { DescriptionsItemSpan, DescriptionsRenderNode } from './types';
|
||||||
|
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import { DESCRIPTIONS_ITEM_NAME } from './use-descriptions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子节点用法的标记组件,本身不渲染任何内容。
|
||||||
|
* 其 props 与默认插槽会被父级 VbenDescriptions 收集为列表项。
|
||||||
|
*/
|
||||||
|
const VbenDescriptionsItem = defineComponent({
|
||||||
|
name: DESCRIPTIONS_ITEM_NAME,
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
default: undefined,
|
||||||
|
type: [
|
||||||
|
String,
|
||||||
|
Number,
|
||||||
|
Function,
|
||||||
|
Object,
|
||||||
|
] as PropType<DescriptionsRenderNode>,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
default: undefined,
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
default: undefined,
|
||||||
|
type: [
|
||||||
|
String,
|
||||||
|
Number,
|
||||||
|
Function,
|
||||||
|
Object,
|
||||||
|
] as PropType<DescriptionsRenderNode>,
|
||||||
|
},
|
||||||
|
labelStyle: {
|
||||||
|
default: undefined,
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
span: {
|
||||||
|
default: undefined,
|
||||||
|
type: [Number, String, Object] as PropType<DescriptionsItemSpan>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return () => null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 额外标记,便于在 vnode 中稳健识别
|
||||||
|
(VbenDescriptionsItem as Record<string, any>).__isDescriptionsItem = true;
|
||||||
|
|
||||||
|
export default VbenDescriptionsItem;
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
import type { DescriptionsSize, InternalDescriptionsItem } from './types';
|
||||||
|
|
||||||
|
import DescriptionsCell from './descriptions-cell.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bordered?: boolean;
|
||||||
|
colon?: boolean;
|
||||||
|
contentStyle?: CSSProperties;
|
||||||
|
labelStyle?: CSSProperties;
|
||||||
|
row: InternalDescriptionsItem[];
|
||||||
|
size?: DescriptionsSize;
|
||||||
|
vertical?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
bordered: false,
|
||||||
|
colon: true,
|
||||||
|
contentStyle: undefined,
|
||||||
|
labelStyle: undefined,
|
||||||
|
size: 'middle',
|
||||||
|
vertical: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function mergeStyle(
|
||||||
|
base?: CSSProperties,
|
||||||
|
override?: CSSProperties,
|
||||||
|
): CSSProperties | undefined {
|
||||||
|
if (!base && !override) return undefined;
|
||||||
|
return { ...base, ...override };
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 垂直布局:标签独占一行,内容独占一行 -->
|
||||||
|
<template v-if="vertical">
|
||||||
|
<tr>
|
||||||
|
<DescriptionsCell
|
||||||
|
v-for="(item, index) in row"
|
||||||
|
:key="`label-${item.key ?? index}`"
|
||||||
|
tag="th"
|
||||||
|
type="label"
|
||||||
|
:span="item.span ?? 1"
|
||||||
|
:bordered="bordered"
|
||||||
|
:colon="colon"
|
||||||
|
:size="size"
|
||||||
|
:label="item.label ?? null"
|
||||||
|
:item-class="item.class"
|
||||||
|
:label-style="mergeStyle(labelStyle, item.labelStyle)"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<DescriptionsCell
|
||||||
|
v-for="(item, index) in row"
|
||||||
|
:key="`content-${item.key ?? index}`"
|
||||||
|
tag="td"
|
||||||
|
type="content"
|
||||||
|
:span="item.span ?? 1"
|
||||||
|
:bordered="bordered"
|
||||||
|
:size="size"
|
||||||
|
:content="item.content ?? null"
|
||||||
|
:item-class="item.class"
|
||||||
|
:content-style="mergeStyle(contentStyle, item.contentStyle)"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 水平 + 边框:每项拆分为 label(th) 与 content(td) -->
|
||||||
|
<tr v-else-if="bordered">
|
||||||
|
<template v-for="(item, index) in row" :key="item.key ?? index">
|
||||||
|
<DescriptionsCell
|
||||||
|
tag="th"
|
||||||
|
type="label"
|
||||||
|
:span="1"
|
||||||
|
:bordered="true"
|
||||||
|
:size="size"
|
||||||
|
:label="item.label ?? null"
|
||||||
|
:item-class="item.class"
|
||||||
|
:label-style="mergeStyle(labelStyle, item.labelStyle)"
|
||||||
|
/>
|
||||||
|
<DescriptionsCell
|
||||||
|
tag="td"
|
||||||
|
type="content"
|
||||||
|
:span="(item.span ?? 1) * 2 - 1"
|
||||||
|
:bordered="true"
|
||||||
|
:size="size"
|
||||||
|
:content="item.content ?? null"
|
||||||
|
:content-style="mergeStyle(contentStyle, item.contentStyle)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 水平 + 非边框:每项一个单元格,label 与 content 同列 -->
|
||||||
|
<tr v-else>
|
||||||
|
<DescriptionsCell
|
||||||
|
v-for="(item, index) in row"
|
||||||
|
:key="item.key ?? index"
|
||||||
|
tag="td"
|
||||||
|
type="item"
|
||||||
|
:span="item.span ?? 1"
|
||||||
|
:colon="colon"
|
||||||
|
:size="size"
|
||||||
|
:label="item.label ?? null"
|
||||||
|
:content="item.content ?? null"
|
||||||
|
:item-class="item.class"
|
||||||
|
:label-style="mergeStyle(labelStyle, item.labelStyle)"
|
||||||
|
:content-style="mergeStyle(contentStyle, item.contentStyle)"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue