chore: merge remote changes
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,11 @@ import type { CSSProperties, PropType, Slots } from 'vue';
|
||||
|
||||
import type { DescriptionItemSchema, DescriptionProps } from './typing';
|
||||
|
||||
import { computed, defineComponent, ref, toRefs, unref, useAttrs } from 'vue';
|
||||
import { computed, defineComponent, ref, unref, useAttrs } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { get, isFunction } from '@vben/utils';
|
||||
import { get, getNestedValue, isFunction } from '@vben/utils';
|
||||
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
import { Card, Descriptions } from 'ant-design-vue';
|
||||
|
||||
const props = {
|
||||
bordered: { default: true, type: Boolean },
|
||||
@@ -111,15 +110,17 @@ export default defineComponent({
|
||||
if (!_data) {
|
||||
return null;
|
||||
}
|
||||
const getField = get(_data, field);
|
||||
if (
|
||||
getField &&
|
||||
!Object.prototype.hasOwnProperty.call(toRefs(_data), field)
|
||||
) {
|
||||
return isFunction(render) ? render!('', _data) : '';
|
||||
}
|
||||
const getField = field.includes('.')
|
||||
? (getNestedValue(_data, field) ?? get(_data, field))
|
||||
: get(_data, field);
|
||||
// if (
|
||||
// getField &&
|
||||
// !Object.prototype.hasOwnProperty.call(toRefs(_data), field)
|
||||
// ) {
|
||||
// return isFunction(render) ? render('', _data) : (getField ?? '');
|
||||
// }
|
||||
return isFunction(render)
|
||||
? render!(getField, _data)
|
||||
? render(getField, _data)
|
||||
: (getField ?? '');
|
||||
}
|
||||
|
||||
@@ -171,20 +172,21 @@ export default defineComponent({
|
||||
const extraSlot = getSlot(slots, 'extra');
|
||||
|
||||
return (
|
||||
<ContentWrap
|
||||
class="text-base"
|
||||
headerClass={props.bordered ? 'p-4' : 'border-none p-4'}
|
||||
<Card
|
||||
bodyStyle={{ padding: '8px 0' }}
|
||||
headStyle={{
|
||||
padding: '8px 16px',
|
||||
fontSize: '14px',
|
||||
minHeight: '24px',
|
||||
}}
|
||||
style={{ margin: 0 }}
|
||||
title={title}
|
||||
>
|
||||
{{
|
||||
default: () => content,
|
||||
title: () => (
|
||||
<div class="mb-2 flex w-full items-center justify-between text-base">
|
||||
<div>{title}</div>
|
||||
{extraSlot && <div>{extraSlot}</div>}
|
||||
</div>
|
||||
),
|
||||
extra: () => extraSlot && <div>{extraSlot}</div>,
|
||||
}}
|
||||
</ContentWrap>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getChatConversationMyList,
|
||||
updateChatConversationMy,
|
||||
} from '#/api/ai/chat/conversation';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import RoleRepository from '../role/RoleRepository.vue';
|
||||
|
||||
@@ -249,7 +250,7 @@ async function handleClearConversation() {
|
||||
try {
|
||||
await confirm('确认后对话会全部清空,置顶的对话除外。');
|
||||
await deleteChatConversationMyByUnpinned();
|
||||
message.success('操作成功!');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
// 清空 对话 和 对话内容
|
||||
activeConversationId.value = null;
|
||||
// 获取 对话列表
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
import * as TaskApi from '#/api/bpm/task';
|
||||
import * as UserApi from '#/api/system/user';
|
||||
import { setConfAndFields2 } from '#/components/form-create';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import Signature from './signature.vue';
|
||||
import ProcessInstanceTimeline from './time-line.vue';
|
||||
@@ -426,7 +427,7 @@ async function handleCopy() {
|
||||
await TaskApi.copyTask(data);
|
||||
copyFormRef.value.resetFields();
|
||||
popOverVisible.value.copy = false;
|
||||
message.success('操作成功');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
@@ -448,7 +449,7 @@ async function handleTransfer() {
|
||||
await TaskApi.transferTask(data);
|
||||
transferFormRef.value.resetFields();
|
||||
popOverVisible.value.transfer = false;
|
||||
message.success('操作成功');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
// 2. 加载最新数据
|
||||
reload();
|
||||
} finally {
|
||||
@@ -473,7 +474,7 @@ async function handleDelegate() {
|
||||
await TaskApi.delegateTask(data);
|
||||
popOverVisible.value.delegate = false;
|
||||
delegateFormRef.value.resetFields();
|
||||
message.success('操作成功');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
// 2. 加载最新数据
|
||||
reload();
|
||||
} finally {
|
||||
@@ -496,7 +497,7 @@ async function handlerAddSign(type: string) {
|
||||
userIds: addSignForm.addSignUserIds,
|
||||
};
|
||||
await TaskApi.signCreateTask(data);
|
||||
message.success('操作成功');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
addSignFormRef.value.resetFields();
|
||||
popOverVisible.value.addSign = false;
|
||||
// 2 加载最新数据
|
||||
@@ -523,7 +524,7 @@ async function handleReturn() {
|
||||
await TaskApi.returnTask(data);
|
||||
popOverVisible.value.return = false;
|
||||
returnFormRef.value.resetFields();
|
||||
message.success('操作成功');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
// 2 重新加载数据
|
||||
reload();
|
||||
} finally {
|
||||
@@ -544,7 +545,7 @@ async function handleCancel() {
|
||||
cancelForm.cancelReason,
|
||||
);
|
||||
popOverVisible.value.return = false;
|
||||
message.success('操作成功');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
cancelFormRef.value.resetFields();
|
||||
// 2 重新加载数据
|
||||
reload();
|
||||
|
||||
@@ -176,8 +176,8 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
render: (val, data) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${val.requestMethod} ${val.requestUrl}`;
|
||||
if (val && data?.requestUrl) {
|
||||
return `${val} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
@@ -1,60 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
defineProps<{
|
||||
redisData?: InfraRedisApi.RedisMonitorInfo;
|
||||
}>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: false,
|
||||
column: 6,
|
||||
class: 'mx-4',
|
||||
schema: [
|
||||
{
|
||||
field: 'info.redis_version',
|
||||
label: 'Redis 版本',
|
||||
},
|
||||
{
|
||||
field: 'info.redis_mode',
|
||||
label: '运行模式',
|
||||
render: (val) => (val === 'standalone' ? '单机' : '集群'),
|
||||
},
|
||||
{
|
||||
field: 'info.tcp_port',
|
||||
label: '端口',
|
||||
},
|
||||
{
|
||||
field: 'info.connected_clients',
|
||||
label: '客户端数',
|
||||
},
|
||||
{
|
||||
field: 'info.uptime_in_days',
|
||||
label: '运行时间(天)',
|
||||
},
|
||||
{
|
||||
field: 'info.used_memory_human',
|
||||
label: '使用内存',
|
||||
},
|
||||
{
|
||||
field: 'info.used_cpu_user_children',
|
||||
label: '使用 CPU',
|
||||
render: (val) => Number.parseFloat(val).toFixed(2),
|
||||
},
|
||||
{
|
||||
field: 'info.maxmemory_human',
|
||||
label: '内存配置',
|
||||
},
|
||||
{
|
||||
field: 'info.aof_enabled',
|
||||
label: 'AOF 是否开启',
|
||||
render: (val) => (val === '0' ? '否' : '是'),
|
||||
},
|
||||
{
|
||||
field: 'info.rdb_last_bgsave_status',
|
||||
label: 'RDB 是否成功',
|
||||
},
|
||||
{
|
||||
field: 'dbSize',
|
||||
label: 'Key 数量',
|
||||
},
|
||||
{
|
||||
field: 'info.instantaneous_input_kbps',
|
||||
label: '网络入口/出口',
|
||||
render: (val, data) =>
|
||||
`${val}kps / ${data?.info?.instantaneous_output_kbps}kps`,
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Descriptions
|
||||
:column="6"
|
||||
bordered
|
||||
size="middle"
|
||||
:label-style="{ width: '138px' }"
|
||||
>
|
||||
<Descriptions.Item label="Redis 版本">
|
||||
{{ redisData?.info?.redis_version }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运行模式">
|
||||
{{ redisData?.info?.redis_mode === 'standalone' ? '单机' : '集群' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="端口">
|
||||
{{ redisData?.info?.tcp_port }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="客户端数">
|
||||
{{ redisData?.info?.connected_clients }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运行时间(天)">
|
||||
{{ redisData?.info?.uptime_in_days }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="使用内存">
|
||||
{{ redisData?.info?.used_memory_human }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="使用 CPU">
|
||||
{{
|
||||
redisData?.info
|
||||
? parseFloat(redisData?.info?.used_cpu_user_children).toFixed(2)
|
||||
: ''
|
||||
}}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="内存配置">
|
||||
{{ redisData?.info?.maxmemory_human }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="AOF 是否开启">
|
||||
{{ redisData?.info?.aof_enabled === '0' ? '否' : '是' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="RDB 是否成功">
|
||||
{{ redisData?.info?.rdb_last_bgsave_status }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Key 数量">
|
||||
{{ redisData?.dbSize }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="网络入口/出口">
|
||||
{{ redisData?.info?.instantaneous_input_kbps }}kps /
|
||||
{{ redisData?.info?.instantaneous_output_kbps }}kps
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Descriptions :data="redisData" />
|
||||
</template>
|
||||
|
||||
@@ -94,7 +94,7 @@ function renderMemoryChart() {
|
||||
detail: {
|
||||
show: true,
|
||||
offsetCenter: [0, '50%'],
|
||||
color: 'auto',
|
||||
color: 'inherit',
|
||||
fontSize: 30,
|
||||
formatter: usedMemory,
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { importDeviceTemplate } from '#/api/iot/device/device';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useImportFormSchema } from '../data';
|
||||
|
||||
@@ -64,7 +65,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code !== 0) {
|
||||
message.error(result.msg || '导入失败');
|
||||
message.error(result.msg || $t('ui.actionMessage.operationFailed'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '导入失败');
|
||||
message.error(error.message || $t('ui.actionMessage.operationFailed'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ async function handleStatusChange(
|
||||
message.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -155,7 +155,7 @@ async function handleStatusChange(
|
||||
message.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -264,14 +264,14 @@ onMounted(async () => {
|
||||
/>
|
||||
</template>
|
||||
<template #expand_content="{ row }">
|
||||
<!-- TODO @xingyu:展开的样子,有点丑 -->
|
||||
<Descriptions
|
||||
:column="4"
|
||||
class="m-4"
|
||||
class="mx-32 my-4 w-1/2"
|
||||
:label-style="{
|
||||
width: '100px',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '14px',
|
||||
color: '#99a9bf',
|
||||
}"
|
||||
:content-style="{ width: '100px', fontSize: '14px' }"
|
||||
>
|
||||
|
||||
@@ -104,7 +104,10 @@ export function useInfoFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 价格库存的表单 */
|
||||
export function useSkuFormSchema(): VbenFormSchema[] {
|
||||
export function useSkuFormSchema(
|
||||
propertyList: any[] = [],
|
||||
isDetail: boolean = false,
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
@@ -152,7 +155,51 @@ export function useSkuFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
// TODO @xingyu:待补充商品属性
|
||||
// 单规格时显示的 SkuList
|
||||
{
|
||||
fieldName: 'singleSkuList',
|
||||
label: '',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
// 当 specType 为 false(单规格)时显示
|
||||
show: (values) => values.specType === false,
|
||||
},
|
||||
},
|
||||
// 多规格时显示的商品属性(占位,实际通过插槽渲染)
|
||||
{
|
||||
fieldName: 'productAttributes',
|
||||
label: '商品属性',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
// 当 specType 为 true(多规格)时显示
|
||||
show: (values) => values.specType === true,
|
||||
},
|
||||
},
|
||||
// 多规格 - 批量设置
|
||||
{
|
||||
fieldName: 'batchSkuList',
|
||||
label: '批量设置',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
// 当 specType 为 true(多规格)且 propertyList 有数据时显示,且非详情模式
|
||||
show: (values) =>
|
||||
values.specType === true && propertyList.length > 0 && !isDetail,
|
||||
},
|
||||
},
|
||||
// 多规格 - 规格列表
|
||||
{
|
||||
fieldName: 'multiSkuList',
|
||||
label: '规格列表',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['specType'],
|
||||
// 当 specType 为 true(多规格)且 propertyList 有数据时显示
|
||||
show: (values) => values.specType === true && propertyList.length > 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropertyAndValues, RuleConfig } from './index';
|
||||
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { ContentWrap, Page } from '@vben/common-ui';
|
||||
import { convertToInteger, formatToFraction } from '@vben/utils';
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { convertToInteger, floatToFixed2, formatToFraction } from '@vben/utils';
|
||||
|
||||
import { Button, Tabs } from 'ant-design-vue';
|
||||
import { Button, Card, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createSpu, getSpu, updateSpu } from '#/api/mall/product/spu';
|
||||
@@ -19,12 +21,102 @@ import {
|
||||
useOtherFormSchema,
|
||||
useSkuFormSchema,
|
||||
} from './form-data';
|
||||
import { getPropertyList } from './index';
|
||||
import ProductAttributes from './product-attributes.vue';
|
||||
import ProductPropertyAddForm from './product-property-add-form.vue';
|
||||
import SkuList from './sku-list.vue';
|
||||
|
||||
const spuId = ref<number>();
|
||||
const { params } = useRoute();
|
||||
|
||||
const { params, name } = useRoute();
|
||||
const activeTabName = ref('info');
|
||||
|
||||
function onTabChange(key: string) {
|
||||
activeTabName.value = key;
|
||||
}
|
||||
|
||||
const tabList = ref([
|
||||
{
|
||||
key: 'info',
|
||||
tab: '基础设置',
|
||||
},
|
||||
{
|
||||
key: 'sku',
|
||||
tab: '价格库存',
|
||||
},
|
||||
{
|
||||
key: 'delivery',
|
||||
tab: '物流设置',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
tab: '商品详情',
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
tab: '其它设置',
|
||||
},
|
||||
]);
|
||||
// spu 表单数据
|
||||
const formData = ref<MallSpuApi.Spu>({
|
||||
name: '', // 商品名称
|
||||
categoryId: undefined, // 商品分类
|
||||
keyword: '', // 关键字
|
||||
picUrl: '', // 商品封面图
|
||||
sliderPicUrls: [], // 商品轮播图
|
||||
introduction: '', // 商品简介
|
||||
deliveryTypes: [], // 配送方式数组
|
||||
deliveryTemplateId: undefined, // 运费模版
|
||||
brandId: undefined, // 商品品牌
|
||||
specType: false, // 商品规格
|
||||
subCommissionType: false, // 分销类型
|
||||
skus: [
|
||||
{
|
||||
price: 0, // 商品价格
|
||||
marketPrice: 0, // 市场价
|
||||
costPrice: 0, // 成本价
|
||||
barCode: '', // 商品条码
|
||||
picUrl: '', // 图片地址
|
||||
stock: 0, // 库存
|
||||
weight: 0, // 商品重量
|
||||
volume: 0, // 商品体积
|
||||
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||
secondBrokeragePrice: 0, // 二级分销的佣金
|
||||
},
|
||||
],
|
||||
description: '', // 商品详情
|
||||
sort: 0, // 商品排序
|
||||
giveIntegral: 0, // 赠送积分
|
||||
virtualSalesCount: 0, // 虚拟销量
|
||||
});
|
||||
const propertyList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
const formLoading = ref(true); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const isDetail = ref(false); // 是否查看详情
|
||||
const skuListRef = ref(); // 商品属性列表 Ref
|
||||
|
||||
// sku 相关属性校验规则
|
||||
const ruleConfig: RuleConfig[] = [
|
||||
{
|
||||
name: 'stock',
|
||||
rule: (arg) => arg >= 0,
|
||||
message: '商品库存必须大于等于 1 !!!',
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
rule: (arg) => arg >= 0.01,
|
||||
message: '商品销售价格必须大于等于 0.01 元!!!',
|
||||
},
|
||||
{
|
||||
name: 'marketPrice',
|
||||
rule: (arg) => arg >= 0.01,
|
||||
message: '商品市场价格必须大于等于 0.01 元!!!',
|
||||
},
|
||||
{
|
||||
name: 'costPrice',
|
||||
rule: (arg) => arg >= 0.01,
|
||||
message: '商品成本价格必须大于等于 0.00 元!!!',
|
||||
},
|
||||
];
|
||||
|
||||
const [InfoForm, infoFormApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
@@ -47,8 +139,23 @@ const [SkuForm, skuFormApi] = useVbenForm({
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useSkuFormSchema(),
|
||||
schema: useSkuFormSchema(propertyList.value, isDetail.value),
|
||||
showDefaultActions: false,
|
||||
handleValuesChange: (values, fieldsChanged) => {
|
||||
if (fieldsChanged.includes('subCommissionType')) {
|
||||
formData.value.subCommissionType = values.subCommissionType;
|
||||
changeSubCommissionType();
|
||||
}
|
||||
if (fieldsChanged.includes('specType')) {
|
||||
formData.value.specType = values.specType;
|
||||
onChangeSpec();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [ProductPropertyAddFormModal, productPropertyAddFormApi] = useVbenModal({
|
||||
connectedComponent: ProductPropertyAddForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DeliveryForm, deliveryFormApi] = useVbenForm({
|
||||
@@ -97,8 +204,15 @@ async function onSubmit() {
|
||||
.merge(descriptionFormApi)
|
||||
.merge(otherFormApi)
|
||||
.submitAllForm(true);
|
||||
|
||||
values.skus = formData.value.skus;
|
||||
if (values.skus) {
|
||||
try {
|
||||
// 校验 sku
|
||||
skuListRef.value.validateSku();
|
||||
} catch {
|
||||
message.error('【库存价格】不完善,请填写相关信息');
|
||||
return;
|
||||
}
|
||||
values.skus.forEach((item) => {
|
||||
// sku相关价格元转分
|
||||
item.price = convertToInteger(item.price);
|
||||
@@ -121,57 +235,176 @@ async function onSubmit() {
|
||||
await (spuId.value ? updateSpu(values) : createSpu(values));
|
||||
}
|
||||
|
||||
async function initDate() {
|
||||
/** 获得详情 */
|
||||
async function getDetail() {
|
||||
if (name === 'ProductSpuDetail') {
|
||||
isDetail.value = true;
|
||||
}
|
||||
const id = params.id as unknown as number;
|
||||
if (id) {
|
||||
try {
|
||||
const res = await getSpu(spuId.value!);
|
||||
res.skus?.forEach((item) => {
|
||||
if (isDetail.value) {
|
||||
item.price = floatToFixed2(item.price);
|
||||
item.marketPrice = floatToFixed2(item.marketPrice);
|
||||
item.costPrice = floatToFixed2(item.costPrice);
|
||||
item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice);
|
||||
} else {
|
||||
// 回显价格分转元
|
||||
item.price = formatToFraction(item.price);
|
||||
item.marketPrice = formatToFraction(item.marketPrice);
|
||||
item.costPrice = formatToFraction(item.costPrice);
|
||||
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = formatToFraction(
|
||||
item.secondBrokeragePrice,
|
||||
);
|
||||
}
|
||||
});
|
||||
formData.value = res;
|
||||
// 初始化各表单值(异步)
|
||||
infoFormApi.setValues(res);
|
||||
skuFormApi.setValues(res);
|
||||
deliveryFormApi.setValues(res);
|
||||
descriptionFormApi.setValues(res);
|
||||
otherFormApi.setValues(res);
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
// 将 SKU 的属性,整理成 PropertyAndValues 数组
|
||||
propertyList.value = getPropertyList(formData.value);
|
||||
}
|
||||
|
||||
// =========== sku form 逻辑 ===========
|
||||
|
||||
function openPropertyAddForm() {
|
||||
productPropertyAddFormApi.open();
|
||||
}
|
||||
|
||||
/** 调用 SkuList generateTableData 方法*/
|
||||
function generateSkus(propertyList: any[]) {
|
||||
skuListRef.value.generateTableData(propertyList);
|
||||
}
|
||||
|
||||
/** 分销类型 */
|
||||
function changeSubCommissionType() {
|
||||
// 默认为零,类型切换后也要重置为零
|
||||
for (const item of formData.value.skus!) {
|
||||
item.firstBrokeragePrice = 0;
|
||||
item.secondBrokeragePrice = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择规格 */
|
||||
function onChangeSpec() {
|
||||
// 重置商品属性列表
|
||||
propertyList.value = [];
|
||||
// 重置sku列表
|
||||
formData.value.skus = [
|
||||
{
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 监听 sku form schema 变化,更新表单
|
||||
watch(
|
||||
propertyList,
|
||||
() => {
|
||||
skuFormApi.updateSchema(
|
||||
useSkuFormSchema(propertyList.value, isDetail.value),
|
||||
);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
spuId.value = params.id as unknown as number;
|
||||
if (!spuId.value) {
|
||||
return;
|
||||
}
|
||||
const res = await getSpu(spuId.value);
|
||||
if (res.skus) {
|
||||
res.skus.forEach((item) => {
|
||||
// 回显价格分转元
|
||||
item.price = formatToFraction(item.price);
|
||||
item.marketPrice = formatToFraction(item.marketPrice);
|
||||
item.costPrice = formatToFraction(item.costPrice);
|
||||
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice);
|
||||
});
|
||||
}
|
||||
infoFormApi.setValues(res);
|
||||
skuFormApi.setValues(res);
|
||||
deliveryFormApi.setValues(res);
|
||||
descriptionFormApi.setValues(res);
|
||||
otherFormApi.setValues(res);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initDate();
|
||||
await getDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ContentWrap class="h-full w-full pb-8">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="onSubmit">保存</Button>
|
||||
</template>
|
||||
<Tabs v-model:active-key="activeTabName">
|
||||
<Tabs.TabPane tab="基础设置" key="info">
|
||||
<InfoForm class="w-3/5" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="价格库存" key="sku">
|
||||
<SkuForm class="w-3/5" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="物流设置" key="delivery">
|
||||
<DeliveryForm class="w-3/5" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="商品详情" key="description">
|
||||
<DescriptionForm class="w-3/5" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="其它设置" key="other">
|
||||
<OtherForm class="w-3/5" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</ContentWrap>
|
||||
</Page>
|
||||
<div>
|
||||
<ProductPropertyAddFormModal :property-list="propertyList" />
|
||||
|
||||
<Page auto-content-height>
|
||||
<Card
|
||||
class="h-full w-full"
|
||||
:loading="formLoading"
|
||||
:tab-list="tabList"
|
||||
:active-key="activeTabName"
|
||||
@tab-change="onTabChange"
|
||||
>
|
||||
<template #tabBarExtraContent>
|
||||
<Button type="primary" @click="onSubmit">保存</Button>
|
||||
</template>
|
||||
<InfoForm class="w-3/5" v-show="activeTabName === 'info'" />
|
||||
|
||||
<SkuForm class="w-full" v-show="activeTabName === 'sku'">
|
||||
<template #singleSkuList>
|
||||
<SkuList
|
||||
ref="skuListRef"
|
||||
:prop-form-data="formData"
|
||||
:property-list="propertyList"
|
||||
:rule-config="ruleConfig"
|
||||
/>
|
||||
</template>
|
||||
<template #productAttributes>
|
||||
<div>
|
||||
<Button class="mb-10px mr-15px" @click="openPropertyAddForm">
|
||||
添加属性
|
||||
</Button>
|
||||
<ProductAttributes
|
||||
:is-detail="isDetail"
|
||||
:property-list="propertyList"
|
||||
@success="generateSkus"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #batchSkuList>
|
||||
<SkuList
|
||||
:is-batch="true"
|
||||
:prop-form-data="formData"
|
||||
:property-list="propertyList"
|
||||
/>
|
||||
</template>
|
||||
<template #multiSkuList>
|
||||
<SkuList
|
||||
ref="skuListRef"
|
||||
:is-detail="isDetail"
|
||||
:prop-form-data="formData"
|
||||
:property-list="propertyList"
|
||||
:rule-config="ruleConfig"
|
||||
/>
|
||||
</template>
|
||||
</SkuForm>
|
||||
<DeliveryForm class="w-3/5" v-show="activeTabName === 'delivery'" />
|
||||
<DescriptionForm
|
||||
class="w-3/5"
|
||||
v-show="activeTabName === 'description'"
|
||||
/>
|
||||
<OtherForm class="w-3/5" v-show="activeTabName === 'other'" />
|
||||
</Card>
|
||||
</Page>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-tabs-tab-btn) {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
65
apps/web-antd/src/views/mall/product/spu/modules/index.ts
Normal file
65
apps/web-antd/src/views/mall/product/spu/modules/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
export interface PropertyAndValues {
|
||||
id: number;
|
||||
name: string;
|
||||
values?: PropertyAndValues[];
|
||||
}
|
||||
|
||||
export interface RuleConfig {
|
||||
// 需要校验的字段
|
||||
// 例:name: 'name' 则表示校验 sku.name 的值
|
||||
// 例:name: 'productConfig.stock' 则表示校验 sku.productConfig.name 的值,此处 productConfig 表示我在 Sku 上扩展的属性
|
||||
name: string;
|
||||
// 校验规格为一个毁掉函数,其中 arg 为需要校验的字段的值。
|
||||
// 例:需要校验价格必须大于0.01
|
||||
// {
|
||||
// name:'price',
|
||||
// rule:(arg: number) => arg > 0.01
|
||||
// }
|
||||
rule: (arg: any) => boolean;
|
||||
// 校验不通过时的消息提示
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得商品的规格列表 - 商品相关的公共函数
|
||||
*
|
||||
* @param spu
|
||||
* @return PropertyAndValues 规格列表
|
||||
*/
|
||||
const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
|
||||
// 直接拿返回的 skus 属性逆向生成出 propertyList
|
||||
const properties: PropertyAndValues[] = [];
|
||||
// 只有是多规格才处理
|
||||
if (spu.specType) {
|
||||
spu.skus?.forEach((sku) => {
|
||||
sku.properties?.forEach(
|
||||
({ propertyId, propertyName, valueId, valueName }) => {
|
||||
// 添加属性
|
||||
if (!properties?.some((item) => item.id === propertyId)) {
|
||||
properties.push({
|
||||
id: propertyId!,
|
||||
name: propertyName!,
|
||||
values: [],
|
||||
});
|
||||
}
|
||||
// 添加属性值
|
||||
const index = properties?.findIndex((item) => item.id === propertyId);
|
||||
if (
|
||||
!properties[index]?.values?.some((value) => value.id === valueId)
|
||||
) {
|
||||
properties[index]?.values?.push({ id: valueId!, name: valueName! });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return properties;
|
||||
};
|
||||
|
||||
export { getPropertyList };
|
||||
|
||||
// 导出组件
|
||||
export { default as SkuList } from './sku-list.vue';
|
||||
@@ -0,0 +1,213 @@
|
||||
<!-- 商品发布 - 库存价格 - 属性列表 -->
|
||||
<script lang="ts" setup>
|
||||
import type { PropertyAndValues } from './index';
|
||||
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Button, Col, Divider, message, Select, Tag } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createPropertyValue,
|
||||
getPropertyValueSimpleList,
|
||||
} from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'ProductAttributes' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
propertyList: () => [],
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
/** 输入框失去焦点或点击回车时触发 */
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
interface Props {
|
||||
propertyList?: PropertyAndValues[];
|
||||
isDetail?: boolean;
|
||||
}
|
||||
|
||||
const inputValue = ref<string[]>([]); // 输入框值(tags 模式使用数组)
|
||||
const attributeIndex = ref<null | number>(null); // 获取焦点时记录当前属性项的index
|
||||
// 输入框显隐控制
|
||||
const inputVisible = computed(() => (index: number) => {
|
||||
if (attributeIndex.value === null) return false;
|
||||
if (attributeIndex.value === index) return true;
|
||||
});
|
||||
|
||||
interface InputRefItem {
|
||||
inputRef?: {
|
||||
attributes: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
const inputRef = ref<InputRefItem[]>([]); // 标签输入框Ref
|
||||
/** 解决 ref 在 v-for 中的获取问题*/
|
||||
function setInputRef(el: any) {
|
||||
if (el === null || el === undefined) return;
|
||||
// 如果不存在 id 相同的元素才添加
|
||||
if (
|
||||
!inputRef.value.some(
|
||||
(item) => item.inputRef?.attributes.id === el.inputRef?.attributes.id,
|
||||
)
|
||||
) {
|
||||
inputRef.value.push(el);
|
||||
}
|
||||
}
|
||||
const attributeList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
const attributeOptions = ref<MallPropertyApi.PropertyValue[]>([]); // 商品属性值下拉框
|
||||
|
||||
watch(
|
||||
() => props.propertyList,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
attributeList.value = data;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 删除属性值*/
|
||||
function handleCloseValue(index: number, valueIndex: number) {
|
||||
attributeList.value?.[index]?.values?.splice(valueIndex, 1);
|
||||
}
|
||||
|
||||
/** 删除属性*/
|
||||
function handleCloseProperty(index: number) {
|
||||
attributeList.value?.splice(index, 1);
|
||||
emit('success', attributeList.value);
|
||||
}
|
||||
|
||||
/** 显示输入框并获取焦点 */
|
||||
async function showInput(index: number) {
|
||||
attributeIndex.value = index;
|
||||
inputRef.value?.[index]?.focus();
|
||||
// 获取属性下拉选项
|
||||
await getAttributeOptions(attributeList.value?.[index]?.id!);
|
||||
}
|
||||
|
||||
// 定义 success 事件,用于操作成功后的回调
|
||||
async function handleInputConfirm(index: number, propertyId: number) {
|
||||
// 从数组中取最后一个输入的值(tags 模式下 inputValue 是数组)
|
||||
const currentValue = inputValue.value?.[inputValue.value.length - 1]?.trim();
|
||||
|
||||
if (currentValue) {
|
||||
// 1. 重复添加校验
|
||||
if (
|
||||
attributeList.value?.[index]?.values?.find(
|
||||
(item) => item.name === currentValue,
|
||||
)
|
||||
) {
|
||||
message.warning('已存在相同属性值,请重试');
|
||||
attributeIndex.value = null;
|
||||
inputValue.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 情况一:属性值已存在,则直接使用并结束
|
||||
const existValue = attributeOptions.value.find(
|
||||
(item) => item.name === currentValue,
|
||||
);
|
||||
if (existValue) {
|
||||
attributeIndex.value = null;
|
||||
inputValue.value = [];
|
||||
attributeList.value?.[index]?.values?.push({
|
||||
id: existValue.id!,
|
||||
name: existValue.name,
|
||||
});
|
||||
emit('success', attributeList.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.2 情况二:新属性值,则进行保存
|
||||
try {
|
||||
const id = await createPropertyValue({
|
||||
propertyId,
|
||||
name: currentValue,
|
||||
});
|
||||
attributeList.value?.[index]?.values?.push({
|
||||
id,
|
||||
name: currentValue,
|
||||
});
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
emit('success', attributeList.value);
|
||||
} catch {
|
||||
message.error($t('ui.actionMessage.operationFailed'));
|
||||
}
|
||||
}
|
||||
attributeIndex.value = null;
|
||||
inputValue.value = [];
|
||||
}
|
||||
|
||||
/** 获取商品属性下拉选项 */
|
||||
async function getAttributeOptions(propertyId: number) {
|
||||
attributeOptions.value = await getPropertyValueSimpleList(propertyId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Col v-for="(item, index) in attributeList" :key="index">
|
||||
<div>
|
||||
<span class="mx-1">属性名:</span>
|
||||
<Tag
|
||||
:closable="!isDetail"
|
||||
class="mx-1"
|
||||
color="success"
|
||||
@close="handleCloseProperty(index)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mx-1">属性值:</span>
|
||||
<Tag
|
||||
v-for="(value, valueIndex) in item.values"
|
||||
:key="value.id"
|
||||
:closable="!isDetail"
|
||||
class="mx-1"
|
||||
@close="handleCloseValue(index, valueIndex)"
|
||||
>
|
||||
{{ value.name }}
|
||||
</Tag>
|
||||
<Select
|
||||
v-show="inputVisible(index)"
|
||||
:id="`input${index}`"
|
||||
:ref="setInputRef"
|
||||
v-model:value="inputValue"
|
||||
allow-clear
|
||||
mode="tags"
|
||||
:max-tag-count="1"
|
||||
:filter-option="true"
|
||||
size="small"
|
||||
style="width: 100px"
|
||||
@blur="handleInputConfirm(index, item.id)"
|
||||
@change="handleInputConfirm(index, item.id)"
|
||||
@keyup.enter="handleInputConfirm(index, item.id)"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="item2 in attributeOptions"
|
||||
:key="item2.id"
|
||||
:value="item2.name"
|
||||
>
|
||||
{{ item2.name }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
v-show="!inputVisible(index)"
|
||||
class="button-new-tag ml-1"
|
||||
size="small"
|
||||
@click="showInput(index)"
|
||||
>
|
||||
+ 添加
|
||||
</Button>
|
||||
</div>
|
||||
<Divider class="my-10px" />
|
||||
</Col>
|
||||
</template>
|
||||
@@ -0,0 +1,153 @@
|
||||
<!-- 商品发布 - 库存价格 - 添加属性 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createProperty,
|
||||
getPropertySimpleList,
|
||||
} from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'ProductPropertyAddForm' });
|
||||
|
||||
const props = defineProps({
|
||||
propertyList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const attributeList = ref<any[]>([]); // 商品属性列表
|
||||
const attributeOptions = ref<MallPropertyApi.Property[]>([]); // 商品属性名称下拉框
|
||||
|
||||
watch(
|
||||
() => props.propertyList,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
attributeList.value = data as any[];
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
// 表单配置
|
||||
const formSchema: VbenFormSchema[] = [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '属性名称',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getPropertySimpleList();
|
||||
attributeOptions.value = data;
|
||||
return data.map((item: MallPropertyApi.Property) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}));
|
||||
},
|
||||
showSearch: true,
|
||||
filterOption: true,
|
||||
placeholder: '请选择属性名称。如果不存在,可手动输入选择',
|
||||
// 支持手动输入新选项
|
||||
mode: 'tags',
|
||||
maxTagCount: 1,
|
||||
allowClear: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化表单
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: formSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 初始化弹窗
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
|
||||
const values = await formApi.getValues();
|
||||
const name = Array.isArray(values.name) ? values.name[0] : values.name;
|
||||
|
||||
// 重复添加校验
|
||||
for (const attrItem of attributeList.value) {
|
||||
if (attrItem.name === name) {
|
||||
message.error('该属性已存在,请勿重复添加');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 情况一:属性名已存在,则直接使用
|
||||
const existProperty = attributeOptions.value.find(
|
||||
(item: MallPropertyApi.Property) => item.name === name,
|
||||
);
|
||||
if (existProperty) {
|
||||
attributeList.value.push({
|
||||
id: existProperty.id,
|
||||
name,
|
||||
values: [],
|
||||
});
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况二:如果是不存在的属性,则需要执行新增
|
||||
try {
|
||||
const data = { name } as MallPropertyApi.Property;
|
||||
const propertyId = await createProperty(data);
|
||||
// 添加到属性列表
|
||||
attributeList.value.push({
|
||||
id: propertyId,
|
||||
name,
|
||||
values: [],
|
||||
});
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
// 发生错误时不关闭弹窗
|
||||
console.error('添加属性失败:', error);
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 重置表单
|
||||
await formApi.resetForm();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="添加商品属性">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
612
apps/web-antd/src/views/mall/product/spu/modules/sku-list.vue
Normal file
612
apps/web-antd/src/views/mall/product/spu/modules/sku-list.vue
Normal file
@@ -0,0 +1,612 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { PropertyAndValues, RuleConfig } from './index';
|
||||
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { copyValueToTarget, formatToFraction, isEmpty } from '@vben/utils';
|
||||
|
||||
import { Button, Image, Input, InputNumber, message } from 'ant-design-vue';
|
||||
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import ImageUpload from '#/components/upload/image-upload.vue';
|
||||
|
||||
defineOptions({ name: 'SkuList' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isActivityComponent?: boolean; // 是否作为 sku 活动配置组件
|
||||
isBatch?: boolean; // 是否作为批量操作组件
|
||||
isComponent?: boolean; // 是否作为 sku 选择组件
|
||||
isDetail?: boolean; // 是否作为 sku 详情组件
|
||||
propertyList?: PropertyAndValues[];
|
||||
propFormData?: MallSpuApi.Spu;
|
||||
ruleConfig?: RuleConfig[];
|
||||
}>(),
|
||||
{
|
||||
propFormData: () => ({}) as MallSpuApi.Spu,
|
||||
propertyList: () => [],
|
||||
ruleConfig: () => [],
|
||||
isBatch: false,
|
||||
isDetail: false,
|
||||
isComponent: false,
|
||||
isActivityComponent: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'selectionChange', value: MallSpuApi.Sku[]): void;
|
||||
}>();
|
||||
|
||||
const { isBatch, isDetail, isComponent, isActivityComponent } = props;
|
||||
|
||||
const formData: Ref<MallSpuApi.Spu | undefined> = ref<MallSpuApi.Spu>(); // 表单数据
|
||||
const skuList = ref<MallSpuApi.Sku[]>([
|
||||
{
|
||||
price: 0, // 商品价格
|
||||
marketPrice: 0, // 市场价
|
||||
costPrice: 0, // 成本价
|
||||
barCode: '', // 商品条码
|
||||
picUrl: '', // 图片地址
|
||||
stock: 0, // 库存
|
||||
weight: 0, // 商品重量
|
||||
volume: 0, // 商品体积
|
||||
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||
secondBrokeragePrice: 0, // 二级分销的佣金
|
||||
},
|
||||
]); // 批量添加时的临时数据
|
||||
|
||||
/** 批量添加 */
|
||||
function batchAdd() {
|
||||
validateProperty();
|
||||
formData.value!.skus!.forEach((item: MallSpuApi.Sku) => {
|
||||
copyValueToTarget(item, skuList.value[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/** 校验商品属性属性值 */
|
||||
function validateProperty() {
|
||||
// 校验商品属性属性值是否为空,有一个为空都不给过
|
||||
const warningInfo = '存在属性属性值为空,请先检查完善属性值后重试!!!';
|
||||
for (const item of props.propertyList as PropertyAndValues[]) {
|
||||
if (!item.values || isEmpty(item.values)) {
|
||||
message.warning(warningInfo);
|
||||
throw new Error(warningInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除 sku */
|
||||
function deleteSku(row: MallSpuApi.Sku) {
|
||||
const index = formData.value!.skus!.findIndex(
|
||||
// 直接把列表转成字符串比较
|
||||
(sku: MallSpuApi.Sku) =>
|
||||
JSON.stringify(sku.properties) === JSON.stringify(row.properties),
|
||||
);
|
||||
formData.value!.skus!.splice(index, 1);
|
||||
}
|
||||
|
||||
const tableHeaders = ref<{ label: string; prop: string }[]>([]); // 多属性表头
|
||||
|
||||
/**
|
||||
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
|
||||
*/
|
||||
function validateSku() {
|
||||
validateProperty();
|
||||
let warningInfo = '请检查商品各行相关属性配置,';
|
||||
let validate = true; // 默认通过
|
||||
for (const sku of formData.value!.skus!) {
|
||||
// 作为活动组件的校验
|
||||
for (const rule of props?.ruleConfig as RuleConfig[]) {
|
||||
const arg = getValue(sku, rule.name);
|
||||
if (!rule.rule(arg)) {
|
||||
validate = false; // 只要有一个不通过则直接不通过
|
||||
warningInfo += rule.message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 只要有一个不通过则结束后续的校验
|
||||
if (!validate) {
|
||||
message.warning(warningInfo);
|
||||
throw new Error(warningInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getValue(obj: any, arg: string): unknown {
|
||||
const keys = arg.split('.');
|
||||
let value: any = obj;
|
||||
for (const key of keys) {
|
||||
if (value && typeof value === 'object' && key in value) {
|
||||
value = value[key];
|
||||
} else {
|
||||
value = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择时触发
|
||||
* @param records 传递过来的选中的 sku 是一个数组
|
||||
*/
|
||||
function handleSelectionChange({ records }: { records: MallSpuApi.Sku[] }) {
|
||||
emit('selectionChange', records);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给 skuList
|
||||
*/
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
formData.value = data;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 生成表数据 */
|
||||
function generateTableData(propertyList: PropertyAndValues[]) {
|
||||
// 构建数据结构
|
||||
const propertyValues = propertyList.map((item: PropertyAndValues) =>
|
||||
(item.values || []).map((v: { id: number; name: string }) => ({
|
||||
propertyId: item.id,
|
||||
propertyName: item.name,
|
||||
valueId: v.id,
|
||||
valueName: v.name,
|
||||
})),
|
||||
);
|
||||
const buildSkuList = build(propertyValues);
|
||||
// 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表
|
||||
if (!validateData(propertyList)) {
|
||||
// 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表
|
||||
formData.value!.skus = [];
|
||||
}
|
||||
for (const item of buildSkuList) {
|
||||
const row = {
|
||||
properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0,
|
||||
};
|
||||
// 如果存在属性相同的 sku 则不做处理
|
||||
const index = formData.value!.skus!.findIndex(
|
||||
(sku: MallSpuApi.Sku) =>
|
||||
JSON.stringify(sku.properties) === JSON.stringify(row.properties),
|
||||
);
|
||||
if (index !== -1) {
|
||||
continue;
|
||||
}
|
||||
formData.value!.skus!.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 skus 前置校验
|
||||
*/
|
||||
function validateData(propertyList: PropertyAndValues[]): boolean {
|
||||
const skuPropertyIds: number[] = [];
|
||||
formData.value!.skus!.forEach((sku: MallSpuApi.Sku) =>
|
||||
sku.properties
|
||||
?.map((property: MallSpuApi.Property) => property.propertyId)
|
||||
?.forEach((propertyId?: number) => {
|
||||
if (!skuPropertyIds.includes(propertyId!)) {
|
||||
skuPropertyIds.push(propertyId!);
|
||||
}
|
||||
}),
|
||||
);
|
||||
const propertyIds = propertyList.map((item: PropertyAndValues) => item.id);
|
||||
return skuPropertyIds.length === propertyIds.length;
|
||||
}
|
||||
|
||||
/** 构建所有排列组合 */
|
||||
function build(
|
||||
propertyValuesList: MallSpuApi.Property[][],
|
||||
): (MallSpuApi.Property | MallSpuApi.Property[])[] {
|
||||
if (propertyValuesList.length === 0) {
|
||||
return [];
|
||||
} else if (propertyValuesList.length === 1) {
|
||||
return propertyValuesList[0] || [];
|
||||
} else {
|
||||
const result: MallSpuApi.Property[][] = [];
|
||||
const rest = build(propertyValuesList.slice(1));
|
||||
const firstList = propertyValuesList[0];
|
||||
if (!firstList) return [];
|
||||
|
||||
for (const element of firstList) {
|
||||
for (const element_ of rest) {
|
||||
// 第一次不是数组结构,后面的都是数组结构
|
||||
if (Array.isArray(element_)) {
|
||||
result.push([element!, ...(element_ as MallSpuApi.Property[])]);
|
||||
} else {
|
||||
result.push([element!, element_ as MallSpuApi.Property]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听属性列表,生成相关参数和表头 */
|
||||
watch(
|
||||
() => props.propertyList as PropertyAndValues[],
|
||||
(propertyList: PropertyAndValues[]) => {
|
||||
// 如果不是多规格则结束
|
||||
if (!formData.value!.specType) {
|
||||
return;
|
||||
}
|
||||
// 如果当前组件作为批量添加数据使用,则重置表数据
|
||||
if (props.isBatch) {
|
||||
skuList.value = [
|
||||
{
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 判断代理对象是否为空
|
||||
if (JSON.stringify(propertyList) === '[]') {
|
||||
return;
|
||||
}
|
||||
// 重置表头
|
||||
tableHeaders.value = [];
|
||||
// 生成表头
|
||||
propertyList.forEach((item, index) => {
|
||||
// name加属性项index区分属性值
|
||||
tableHeaders.value.push({ prop: `name${index}`, label: item.name });
|
||||
});
|
||||
// 如果回显的 sku 属性和添加的属性一致则不处理
|
||||
if (validateData(propertyList)) {
|
||||
return;
|
||||
}
|
||||
// 添加新属性没有属性值也不做处理
|
||||
if (propertyList.some((item) => !item.values || isEmpty(item.values))) {
|
||||
return;
|
||||
}
|
||||
// 生成 table 数据,即 sku 列表
|
||||
generateTableData(propertyList);
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
const activitySkuListRef = ref();
|
||||
|
||||
function getSkuTableRef() {
|
||||
return activitySkuListRef.value;
|
||||
}
|
||||
|
||||
// 暴露出生成 sku 方法,给添加属性成功时调用
|
||||
defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 情况一:添加/修改 -->
|
||||
<VxeTable
|
||||
v-if="!isDetail && !isActivityComponent"
|
||||
:data="isBatch ? skuList : formData?.skus || []"
|
||||
border
|
||||
max-height="500"
|
||||
size="small"
|
||||
class="w-full"
|
||||
>
|
||||
<VxeColumn align="center" title="图片" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<ImageUpload
|
||||
v-model:value="row.picUrl"
|
||||
:max-number="1"
|
||||
:max-size="2"
|
||||
:show-description="false"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.specType && !isBatch">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<VxeColumn
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
:title="item.label"
|
||||
align="center"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="font-bold text-[#40aaff]">
|
||||
{{ row.properties?.[index]?.valueName }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</template>
|
||||
<VxeColumn align="center" title="商品条码" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<Input v-model:value="row.barCode" class="w-full" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="销售价" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="市场价" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.marketPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="成本价" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.costPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="库存" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber v-model:value="row.stock" :min="0" class="w-full" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="重量(kg)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.weight"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="体积(m^3)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.volume"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.subCommissionType">
|
||||
<VxeColumn align="center" title="一级返佣(元)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.firstBrokeragePrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="二级返佣(元)" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.secondBrokeragePrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</template>
|
||||
<VxeColumn
|
||||
v-if="formData?.specType"
|
||||
align="center"
|
||||
fixed="right"
|
||||
title="操作"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<Button v-if="isBatch" type="link" size="small" @click="batchAdd">
|
||||
批量添加
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="deleteSku(row)"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
|
||||
<!-- 情况二:详情 -->
|
||||
<VxeTable
|
||||
v-if="isDetail"
|
||||
ref="activitySkuListRef"
|
||||
:data="formData?.skus || []"
|
||||
border
|
||||
max-height="500"
|
||||
size="small"
|
||||
class="w-full"
|
||||
:checkbox-config="isComponent ? { reserve: true } : undefined"
|
||||
@checkbox-change="handleSelectionChange"
|
||||
@checkbox-all="handleSelectionChange"
|
||||
>
|
||||
<VxeColumn v-if="isComponent" type="checkbox" width="45" />
|
||||
<VxeColumn align="center" title="图片" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<Image
|
||||
v-if="row.picUrl"
|
||||
:src="row.picUrl"
|
||||
class="h-[50px] w-[50px] cursor-pointer"
|
||||
:preview="true"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.specType && !isBatch">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<VxeColumn
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
:title="item.label"
|
||||
align="center"
|
||||
min-width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="font-bold text-[#40aaff]">
|
||||
{{ row.properties?.[index]?.valueName }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</template>
|
||||
<VxeColumn align="center" title="商品条码" min-width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.barCode }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="销售价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.price }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="市场价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.marketPrice }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="成本价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.costPrice }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="库存" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.stock }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="重量(kg)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.weight }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="体积(m^3)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.volume }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.subCommissionType">
|
||||
<VxeColumn align="center" title="一级返佣(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.firstBrokeragePrice }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="二级返佣(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.secondBrokeragePrice }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</template>
|
||||
</VxeTable>
|
||||
|
||||
<!-- 情况三:作为活动组件 -->
|
||||
<VxeTable
|
||||
v-if="isActivityComponent"
|
||||
:data="formData?.skus || []"
|
||||
border
|
||||
max-height="500"
|
||||
size="small"
|
||||
class="w-full"
|
||||
>
|
||||
<VxeColumn v-if="isComponent" type="checkbox" width="45" />
|
||||
<VxeColumn align="center" title="图片" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<Image
|
||||
:src="row.picUrl"
|
||||
class="h-[60px] w-[60px] cursor-pointer"
|
||||
:preview="true"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.specType">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<VxeColumn
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
:title="item.label"
|
||||
align="center"
|
||||
min-width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="font-bold text-[#40aaff]">
|
||||
{{ row.properties?.[index]?.valueName }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</template>
|
||||
<VxeColumn align="center" title="商品条码" min-width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.barCode }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="销售价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.price) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="市场价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.marketPrice) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="成本价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.costPrice) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn align="center" title="库存" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.stock }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<!-- 方便扩展每个活动配置的属性不一样 -->
|
||||
<slot name="extension"></slot>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,140 @@
|
||||
<!-- SKU 选择弹窗组件 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { Input, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getSpu } from '#/api/mall/product/spu';
|
||||
|
||||
interface SpuData {
|
||||
spuId: number;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [sku: MallSpuApi.Sku];
|
||||
}>();
|
||||
|
||||
const selectedSkuId = ref<number>();
|
||||
const spuId = ref<number>();
|
||||
|
||||
// 配置列
|
||||
const gridColumns = computed<VxeGridProps['columns']>(() => [
|
||||
{
|
||||
field: 'id',
|
||||
title: '#',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
slots: { default: 'radio-column' },
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '图片',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'properties',
|
||||
title: '规格',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
formatter: ({ cellValue }) => {
|
||||
return (
|
||||
cellValue?.map((p: MallSpuApi.Property) => p.valueName)?.join(' ') ||
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '销售价(元)',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: ({ cellValue }) => {
|
||||
return fenToYuan(cellValue);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 初始化表格
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: gridColumns.value,
|
||||
height: 400,
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
if (!spuId.value) {
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
try {
|
||||
const spu = await getSpu(spuId.value);
|
||||
return {
|
||||
items: spu.skus || [],
|
||||
total: spu.skus?.length || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
message.error('加载 SKU 数据失败');
|
||||
console.error(error);
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 处理选中
|
||||
function handleSelected(row: MallSpuApi.Sku) {
|
||||
emit('change', row);
|
||||
modalApi.close();
|
||||
selectedSkuId.value = undefined;
|
||||
}
|
||||
|
||||
// 初始化弹窗
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
selectedSkuId.value = undefined;
|
||||
spuId.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = modalApi.getData<SpuData>();
|
||||
if (data?.spuId) {
|
||||
spuId.value = data.spuId;
|
||||
// 触发数据查询
|
||||
await gridApi.query();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[700px]" title="选择规格">
|
||||
<Grid>
|
||||
<!-- 单选列 -->
|
||||
<template #radio-column="{ row }">
|
||||
<Input
|
||||
v-model="selectedSkuId"
|
||||
:value="row.id"
|
||||
class="cursor-pointer"
|
||||
type="radio"
|
||||
@change="handleSelected(row)"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -73,7 +73,7 @@ async function handleStatusChange(
|
||||
message.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -125,7 +125,7 @@ async function handleExport() {
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:download" />
|
||||
</template>
|
||||
导出
|
||||
{{ $t('page.action.export') }}
|
||||
</Button>
|
||||
</ShortcutDateRangePicker>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +166,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'extra',
|
||||
label: '操作拓展参数',
|
||||
show: (data) => !data?.extra,
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
field: 'requestUrl',
|
||||
|
||||
@@ -1,79 +1,170 @@
|
||||
<script lang="tsx">
|
||||
import type { DescriptionProps } from 'element-plus';
|
||||
import type { CSSProperties, PropType, Slots } from 'vue';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
import type { DescriptionItemSchema, DescriptionProps } from './typing';
|
||||
|
||||
import type { DescriptionItemSchema, DescriptionsOptions } from './typing';
|
||||
import { computed, defineComponent, ref, unref, useAttrs } from 'vue';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { get } from '@vben/utils';
|
||||
import { get, getNestedValue, isFunction } from '@vben/utils';
|
||||
|
||||
import { ElDescriptions, ElDescriptionsItem } from 'element-plus';
|
||||
|
||||
/** 对 Descriptions 进行二次封装 */
|
||||
const Description = defineComponent({
|
||||
name: 'Descriptions',
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
schema: {
|
||||
type: Array as PropType<DescriptionItemSchema[]>,
|
||||
default: () => [],
|
||||
},
|
||||
// Descriptions 原生 props
|
||||
componentProps: {
|
||||
type: Object as PropType<DescriptionProps>,
|
||||
default: () => ({}),
|
||||
const props = {
|
||||
bordered: { default: true, type: Boolean },
|
||||
column: {
|
||||
default: () => {
|
||||
return { lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4 };
|
||||
},
|
||||
type: [Number, Object],
|
||||
},
|
||||
data: { type: Object },
|
||||
schema: {
|
||||
default: () => [],
|
||||
type: Array as PropType<DescriptionItemSchema[]>,
|
||||
},
|
||||
size: {
|
||||
default: 'default',
|
||||
type: String,
|
||||
validator: (v: string) =>
|
||||
['default', 'middle', 'small', undefined].includes(v),
|
||||
},
|
||||
title: { default: '', type: String },
|
||||
direction: { default: 'horizontal', type: String },
|
||||
};
|
||||
|
||||
setup(props: DescriptionsOptions) {
|
||||
// TODO @puhui999:每个 field 的 slot 的考虑
|
||||
// TODO @puhui999:from 5.0:extra: () => getSlot(slots, 'extra')
|
||||
/** 过滤掉不需要展示的 */
|
||||
const shouldShowItem = (item: DescriptionItemSchema) => {
|
||||
if (item.hidden === undefined) return true;
|
||||
return typeof item.hidden === 'function'
|
||||
? !item.hidden(props.data)
|
||||
: !item.hidden;
|
||||
};
|
||||
/** 渲染内容 */
|
||||
const renderContent = (item: DescriptionItemSchema) => {
|
||||
if (item.content) {
|
||||
return typeof item.content === 'function'
|
||||
? item.content(props.data)
|
||||
: item.content;
|
||||
function getSlot(slots: Slots, slot: string, data?: any) {
|
||||
if (!slots || !Reflect.has(slots, slot)) {
|
||||
return null;
|
||||
}
|
||||
if (!isFunction(slots[slot])) {
|
||||
console.error(`${slot} is not a function!`);
|
||||
return null;
|
||||
}
|
||||
const slotFn = slots[slot];
|
||||
if (!slotFn) return null;
|
||||
return slotFn({ data });
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Description',
|
||||
props,
|
||||
setup(props, { slots }) {
|
||||
const propsRef = ref<null | Partial<DescriptionProps>>(null);
|
||||
|
||||
const prefixCls = 'description';
|
||||
const attrs = useAttrs();
|
||||
|
||||
const getMergeProps = computed(() => {
|
||||
return {
|
||||
...props,
|
||||
...(unref(propsRef) as any),
|
||||
} as DescriptionProps;
|
||||
});
|
||||
|
||||
const getProps = computed(() => {
|
||||
const opt = {
|
||||
...unref(getMergeProps),
|
||||
};
|
||||
return opt as DescriptionProps;
|
||||
});
|
||||
|
||||
const getDescriptionsProps = computed(() => {
|
||||
return { ...unref(attrs), ...unref(getProps) } as DescriptionProps;
|
||||
});
|
||||
|
||||
// 防止换行
|
||||
function renderLabel({
|
||||
label,
|
||||
labelMinWidth,
|
||||
labelStyle,
|
||||
}: DescriptionItemSchema) {
|
||||
if (!labelStyle && !labelMinWidth) {
|
||||
return label;
|
||||
}
|
||||
return item.field ? get(props.data, item.field) : null;
|
||||
};
|
||||
|
||||
return () => (
|
||||
<ElDescriptions
|
||||
{...props}
|
||||
border={props.componentProps?.border}
|
||||
column={props.componentProps?.column}
|
||||
direction={props.componentProps?.direction}
|
||||
extra={props.componentProps?.extra}
|
||||
size={props.componentProps?.size}
|
||||
title={props.componentProps?.title}
|
||||
>
|
||||
{props.schema?.filter(shouldShowItem).map((item) => (
|
||||
<ElDescriptionsItem
|
||||
key={item.field || String(item.label)}
|
||||
label={item.label as string}
|
||||
span={item.span}
|
||||
>
|
||||
{renderContent(item)}
|
||||
</ElDescriptionsItem>
|
||||
))}
|
||||
</ElDescriptions>
|
||||
);
|
||||
const labelStyles: CSSProperties = {
|
||||
...labelStyle,
|
||||
minWidth: `${labelMinWidth}px `,
|
||||
};
|
||||
return <div style={labelStyles}>{label}</div>;
|
||||
}
|
||||
|
||||
function renderItem() {
|
||||
const { data, schema } = unref(getProps);
|
||||
return unref(schema)
|
||||
.map((item) => {
|
||||
const { contentMinWidth, field, render, show, span } = item;
|
||||
|
||||
if (show && isFunction(show) && !show(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
const _data = unref(getProps)?.data;
|
||||
if (!_data) {
|
||||
return null;
|
||||
}
|
||||
const getField = field.includes('.')
|
||||
? (getNestedValue(_data, field) ?? get(_data, field))
|
||||
: get(_data, field);
|
||||
// if (
|
||||
// getField &&
|
||||
// !Object.prototype.hasOwnProperty.call(toRefs(_data), field)
|
||||
// ) {
|
||||
// return isFunction(render) ? render('', _data) : (getField ?? '');
|
||||
// }
|
||||
return isFunction(render)
|
||||
? render(getField, _data)
|
||||
: (getField ?? '');
|
||||
}
|
||||
|
||||
const width = contentMinWidth;
|
||||
return (
|
||||
<ElDescriptionsItem key={field} span={span}>
|
||||
{{
|
||||
label: () => {
|
||||
return renderLabel(item);
|
||||
},
|
||||
default: () => {
|
||||
if (item.slot) {
|
||||
const slotContent = getSlot(slots, item.slot, data);
|
||||
return slotContent;
|
||||
}
|
||||
if (!contentMinWidth) {
|
||||
return getContent();
|
||||
}
|
||||
const style: CSSProperties = {
|
||||
minWidth: `${width}px`,
|
||||
};
|
||||
return <div style={style}>{getContent()}</div>;
|
||||
},
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
);
|
||||
})
|
||||
.filter((item) => !!item);
|
||||
}
|
||||
|
||||
function renderDesc() {
|
||||
const extraSlot = getSlot(slots, 'extra');
|
||||
const slotsObj: any = {
|
||||
default: () => renderItem(),
|
||||
};
|
||||
if (extraSlot) {
|
||||
slotsObj.extra = () => extraSlot;
|
||||
}
|
||||
return (
|
||||
<ElDescriptions
|
||||
class={`${prefixCls}`}
|
||||
title={unref(getMergeProps).title}
|
||||
{...(unref(getDescriptionsProps) as any)}
|
||||
>
|
||||
{slotsObj}
|
||||
</ElDescriptions>
|
||||
);
|
||||
}
|
||||
|
||||
return () => renderDesc();
|
||||
},
|
||||
});
|
||||
|
||||
// TODO @puhui999:from 5.0:emits: ['register'] 事件
|
||||
export default Description;
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import type { DescriptionProps } from 'element-plus';
|
||||
import type { DescriptionProps as ElDescriptionProps } from 'element-plus';
|
||||
import type { JSX } from 'vue/jsx-runtime';
|
||||
|
||||
import type { CSSProperties, VNode } from 'vue';
|
||||
|
||||
// TODO @puhui999:【content】这个纠结下;1)vben2.0 是 render;https://doc.vvbin.cn/components/desc.html#usage 2)
|
||||
// TODO @puhui999:vben2.0 还有 sapn【done】、labelMinWidth、contentMinWidth
|
||||
// TODO @puhui999:【hidden】这个纠结下;1)vben2.0 是 show;
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
export interface DescriptionItemSchema {
|
||||
label: string | VNode; // 内容的描述
|
||||
field?: string; // 对应 data 中的字段名
|
||||
content?: ((data: any) => string | VNode) | string | VNode; // 自定义需要展示的内容,比如说 dict-tag
|
||||
span?: number; // 包含列的数量
|
||||
labelStyle?: CSSProperties; // 自定义标签样式
|
||||
contentStyle?: CSSProperties; // 自定义内容样式
|
||||
hidden?: ((data: any) => boolean) | boolean; // 是否显示
|
||||
labelMinWidth?: number;
|
||||
contentMinWidth?: number;
|
||||
// 自定义标签样式
|
||||
labelStyle?: CSSProperties;
|
||||
// 对应 data 中的字段名
|
||||
field: string;
|
||||
// 内容的描述
|
||||
label: JSX.Element | string | VNode;
|
||||
// 包含列的数量
|
||||
span?: number;
|
||||
// 是否显示
|
||||
show?: (...arg: any) => boolean;
|
||||
// 插槽名称
|
||||
slot?: string;
|
||||
// 自定义需要展示的内容
|
||||
render?: (
|
||||
val: any,
|
||||
data?: Recordable<any>,
|
||||
) => Element | JSX.Element | number | string | undefined | VNode;
|
||||
}
|
||||
|
||||
// TODO @puhui999:vben2.0 还有 title【done】、bordered【done】d、useCollapse、collapseOptions
|
||||
// TODO @puhui999:from 5.0:bordered 默认为 true
|
||||
// TODO @puhui999:from 5.0:column 默认为 lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4
|
||||
// TODO @puhui999:from 5.0:size 默认为 small;有 'default', 'middle', 'small', undefined
|
||||
// TODO @puhui999:from 5.0:useCollapse 默认为 true
|
||||
export interface DescriptionsOptions {
|
||||
data?: Record<string, any>; // 数据
|
||||
schema?: DescriptionItemSchema[]; // 描述项配置
|
||||
componentProps?: DescriptionProps; // antd Descriptions 组件参数
|
||||
export interface DescriptionProps extends ElDescriptionProps {
|
||||
// 描述项配置
|
||||
schema: DescriptionItemSchema[];
|
||||
// 数据
|
||||
data: Recordable<any>;
|
||||
// 是否包含边框
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
}
|
||||
|
||||
@@ -1,71 +1,31 @@
|
||||
import type { DescriptionsOptions } from './typing';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { defineComponent, h, isReactive, reactive, watch } from 'vue';
|
||||
import type { DescInstance, DescriptionProps } from './typing';
|
||||
|
||||
import { h, reactive } from 'vue';
|
||||
|
||||
import Description from './description.vue';
|
||||
|
||||
/** 描述列表 api 定义 */
|
||||
class DescriptionApi {
|
||||
private state = reactive<Record<string, any>>({});
|
||||
export function useDescription(options?: Partial<DescriptionProps>) {
|
||||
const propsState = reactive<Partial<DescriptionProps>>(options || {});
|
||||
|
||||
constructor(options: DescriptionsOptions) {
|
||||
this.state = { ...options };
|
||||
}
|
||||
const api: DescInstance = {
|
||||
setDescProps: (descProps: Partial<DescriptionProps>): void => {
|
||||
Object.assign(propsState, descProps);
|
||||
},
|
||||
};
|
||||
|
||||
getState(): DescriptionsOptions {
|
||||
return this.state as DescriptionsOptions;
|
||||
}
|
||||
|
||||
// TODO @puhui999:【setState】纠结下:1)vben2.0 是 data https://doc.vvbin.cn/components/desc.html#usage;
|
||||
setState(newState: Partial<DescriptionsOptions>) {
|
||||
this.state = { ...this.state, ...newState };
|
||||
}
|
||||
}
|
||||
|
||||
export type ExtendedDescriptionApi = DescriptionApi;
|
||||
|
||||
export function useDescription(options: DescriptionsOptions) {
|
||||
const IS_REACTIVE = isReactive(options);
|
||||
const api = new DescriptionApi(options);
|
||||
// 扩展API
|
||||
const extendedApi: ExtendedDescriptionApi = api as never;
|
||||
const Desc = defineComponent({
|
||||
// 创建一个包装组件,将 propsState 合并到 props 中
|
||||
const DescriptionWrapper: Component = {
|
||||
name: 'UseDescription',
|
||||
inheritAttrs: false,
|
||||
setup(_, { attrs, slots }) {
|
||||
// 合并props和attrs到state
|
||||
api.setState({ ...attrs });
|
||||
|
||||
return () =>
|
||||
h(
|
||||
Description,
|
||||
{
|
||||
...api.getState(),
|
||||
...attrs,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => {
|
||||
// @ts-ignore - 避免类型实例化过深
|
||||
return h(Description, { ...propsState, ...attrs }, slots);
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 响应式支持
|
||||
if (IS_REACTIVE) {
|
||||
watch(
|
||||
() => options.schema,
|
||||
(newSchema) => {
|
||||
api.setState({ schema: newSchema });
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => options.data,
|
||||
(newData) => {
|
||||
api.setState({ data: newData });
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
}
|
||||
|
||||
return [Desc, extendedApi] as const;
|
||||
return [DescriptionWrapper, api] as const;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -181,10 +180,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -197,10 +196,11 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
label: '用户 UA',
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
render: (val, data) => {
|
||||
if (val && data?.requestUrl) {
|
||||
return `${val} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -208,10 +208,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data.requestParams) {
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(data.requestParams),
|
||||
value: JSON.parse(val),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
@@ -224,26 +224,29 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
},
|
||||
{
|
||||
label: '请求时间',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
field: 'beginTime',
|
||||
render: (val, data) => {
|
||||
if (data?.beginTime && data?.endTime) {
|
||||
return `${formatDateTime(data.beginTime)} ~ ${formatDateTime(data.endTime)}`;
|
||||
return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '请求耗时',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
return data?.duration ? `${data.duration} ms` : '';
|
||||
field: 'duration',
|
||||
render: (val) => {
|
||||
return val ? `${val} ms` : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '操作结果',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.resultCode === 0) {
|
||||
field: 'resultCode',
|
||||
render: (val, data) => {
|
||||
if (val === 0) {
|
||||
return '正常';
|
||||
} else if (data && data.resultCode > 0) {
|
||||
return `失败 | ${data.resultCode} | ${data.resultMsg}`;
|
||||
return `失败 | ${val} | ${data.resultMsg}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -259,10 +262,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'operateType',
|
||||
label: '操作类型',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_OPERATE_TYPE,
|
||||
value: data?.operateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,14 +12,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 110,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 110,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -158,10 +157,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -176,9 +175,9 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
render: (val, data) => {
|
||||
if (val && data?.requestUrl) {
|
||||
return `${val} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -186,10 +185,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data.requestParams) {
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(data.requestParams),
|
||||
value: JSON.parse(val),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
@@ -199,9 +198,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'exceptionTime',
|
||||
label: '异常时间',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return formatDateTime(data?.exceptionTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'exceptionName',
|
||||
@@ -210,12 +207,11 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'exceptionStackTrace',
|
||||
label: '异常堆栈',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) =>
|
||||
!data?.exceptionStackTrace,
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data?.exceptionStackTrace) {
|
||||
show: (val) => !val,
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h('textarea', {
|
||||
value: data.exceptionStackTrace,
|
||||
value: val,
|
||||
style:
|
||||
'width: 100%; min-height: 200px; max-height: 400px; resize: vertical;',
|
||||
readonly: true,
|
||||
@@ -227,25 +223,23 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'processStatus',
|
||||
label: '处理状态',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
|
||||
value: data?.processStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'processUserId',
|
||||
label: '处理人',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) => !data?.processUserId,
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
field: 'processTime',
|
||||
label: '处理时间',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) => !data?.processTime,
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return formatDateTime(data?.processTime || '') as string;
|
||||
},
|
||||
show: (val) => !val,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,14 +12,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 110,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 110,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
@@ -197,10 +196,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'status',
|
||||
label: '任务状态',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_JOB_STATUS,
|
||||
value: data?.status,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -222,27 +221,23 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
},
|
||||
{
|
||||
label: '重试间隔',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
return data?.retryInterval ? `${data.retryInterval} 毫秒` : '无间隔';
|
||||
},
|
||||
field: 'retryInterval',
|
||||
render: (val) => (val ? `${val} 毫秒` : '无间隔'),
|
||||
},
|
||||
{
|
||||
label: '监控超时时间',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
return data?.monitorTimeout && data.monitorTimeout > 0
|
||||
? `${data.monitorTimeout} 毫秒`
|
||||
: '未开启';
|
||||
},
|
||||
field: 'monitorTimeout',
|
||||
render: (val) => (val && val > 0 ? `${val} 毫秒` : '未开启'),
|
||||
},
|
||||
{
|
||||
field: 'nextTimes',
|
||||
label: '后续执行时间',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
if (!data?.nextTimes || data.nextTimes.length === 0) {
|
||||
render: (val) => {
|
||||
if (!val || val.length === 0) {
|
||||
return '无后续执行时间';
|
||||
}
|
||||
return h(ElTimeline, {}, () =>
|
||||
data.nextTimes?.map((time: Date) =>
|
||||
val?.map((time: Date) =>
|
||||
h(ElTimelineItem, {}, () => formatDateTime(time)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -154,9 +153,9 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'beginTime',
|
||||
label: '执行时间',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
if (data?.beginTime && data?.endTime) {
|
||||
return `${formatDateTime(data.beginTime)} ~ ${formatDateTime(data.endTime)}`;
|
||||
render: (val, data) => {
|
||||
if (val && data?.endTime) {
|
||||
return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -164,17 +163,15 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'duration',
|
||||
label: '执行时长',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
return data?.duration ? `${data.duration} 毫秒` : '';
|
||||
},
|
||||
render: (val) => (val ? `${val} 毫秒` : ''),
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '任务状态',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_JOB_LOG_STATUS,
|
||||
value: data?.status,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,14 +13,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<InfraJobLogApi.JobLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,14 +14,8 @@ const formData = ref<InfraJobApi.Job>(); // 任务详情
|
||||
const nextTimes = ref<Date[]>([]); // 下一次执行时间
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,55 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraRedisApi } from '#/api/infra/redis';
|
||||
|
||||
import { ElDescriptions, ElDescriptionsItem } from 'element-plus';
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
defineProps<{
|
||||
redisData?: InfraRedisApi.RedisMonitorInfo;
|
||||
}>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: false,
|
||||
column: 6,
|
||||
schema: [
|
||||
{
|
||||
field: 'info.redis_version',
|
||||
label: 'Redis 版本',
|
||||
},
|
||||
{
|
||||
field: 'info.redis_mode',
|
||||
label: '运行模式',
|
||||
render: (val) => (val === 'standalone' ? '单机' : '集群'),
|
||||
},
|
||||
{
|
||||
field: 'info.tcp_port',
|
||||
label: '端口',
|
||||
},
|
||||
{
|
||||
field: 'info.connected_clients',
|
||||
label: '客户端数',
|
||||
},
|
||||
{
|
||||
field: 'info.uptime_in_days',
|
||||
label: '运行时间(天)',
|
||||
},
|
||||
{
|
||||
field: 'info.used_memory_human',
|
||||
label: '使用内存',
|
||||
},
|
||||
{
|
||||
field: 'info.used_cpu_user_children',
|
||||
label: '使用 CPU',
|
||||
render: (val) => Number.parseFloat(val).toFixed(2),
|
||||
},
|
||||
{
|
||||
field: 'info.maxmemory_human',
|
||||
label: '内存配置',
|
||||
},
|
||||
{
|
||||
field: 'info.aof_enabled',
|
||||
label: 'AOF 是否开启',
|
||||
render: (val) => (val === '0' ? '否' : '是'),
|
||||
},
|
||||
{
|
||||
field: 'info.rdb_last_bgsave_status',
|
||||
label: 'RDB 是否成功',
|
||||
},
|
||||
{
|
||||
field: 'dbSize',
|
||||
label: 'Key 数量',
|
||||
},
|
||||
{
|
||||
field: 'info.instantaneous_input_kbps',
|
||||
label: '网络入口/出口',
|
||||
render: (val, data) =>
|
||||
`${val}kps / ${data?.info?.instantaneous_output_kbps}kps`,
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDescriptions :column="6" border size="default" :label-width="138">
|
||||
<ElDescriptionsItem label="Redis 版本">
|
||||
{{ redisData?.info?.redis_version }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运行模式">
|
||||
{{ redisData?.info?.redis_mode === 'standalone' ? '单机' : '集群' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="端口">
|
||||
{{ redisData?.info?.tcp_port }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="客户端数">
|
||||
{{ redisData?.info?.connected_clients }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="运行时间(天)">
|
||||
{{ redisData?.info?.uptime_in_days }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="使用内存">
|
||||
{{ redisData?.info?.used_memory_human }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="使用 CPU">
|
||||
{{
|
||||
redisData?.info
|
||||
? parseFloat(redisData?.info?.used_cpu_user_children).toFixed(2)
|
||||
: ''
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="内存配置">
|
||||
{{ redisData?.info?.maxmemory_human }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="AOF 是否开启">
|
||||
{{ redisData?.info?.aof_enabled === '0' ? '否' : '是' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="RDB 是否成功">
|
||||
{{ redisData?.info?.rdb_last_bgsave_status }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="Key 数量">
|
||||
{{ redisData?.dbSize }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="网络入口/出口">
|
||||
{{ redisData?.info?.instantaneous_input_kbps }}kps /
|
||||
{{ redisData?.info?.instantaneous_output_kbps }}kps
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<Descriptions :data="redisData" />
|
||||
</template>
|
||||
|
||||
@@ -94,7 +94,7 @@ function renderMemoryChart() {
|
||||
detail: {
|
||||
show: true,
|
||||
offsetCenter: [0, '50%'],
|
||||
color: 'auto',
|
||||
color: 'inherit',
|
||||
fontSize: 30,
|
||||
formatter: usedMemory,
|
||||
},
|
||||
|
||||
@@ -80,7 +80,7 @@ async function handleStatusChange(
|
||||
ElMessage.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -145,7 +145,7 @@ async function handleStatusChange(
|
||||
ElMessage.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -70,7 +70,7 @@ async function handleStatusChange(
|
||||
ElMessage.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
reject(new Error($t('ui.actionMessage.operationFailed')));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -133,7 +133,7 @@ async function handleExport() {
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:download" />
|
||||
</template>
|
||||
导出
|
||||
{{ $t('page.action.export') }}
|
||||
</ElButton>
|
||||
</ShortcutDateRangePicker>
|
||||
</div>
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
isSameDay,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { ElButton, ElCard, ElCol, ElRow } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { ElButton, ElCard, ElCol, ElRow } from 'element-plus';
|
||||
|
||||
import * as TradeStatisticsApi from '#/api/mall/statistics/trade';
|
||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||
@@ -123,7 +123,11 @@ async function handleExport() {
|
||||
<!-- 查询条件 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<ShortcutDateRangePicker @change="handleDateRangeChange">
|
||||
<ElButton class="ml-4" @click="handleExport" :loading="exportLoading">
|
||||
<ElButton
|
||||
class="ml-4"
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:download" />
|
||||
</template>
|
||||
@@ -279,4 +283,3 @@ async function handleExport() {
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ElCard } from 'element-plus';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mode?: 'kefu' | 'member';
|
||||
user: MemberUserApi.User;
|
||||
@@ -20,49 +20,44 @@ withDefaults(
|
||||
);
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: false,
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
border: false,
|
||||
column: props.mode === 'member' ? 2 : 1,
|
||||
labelWidth: 140,
|
||||
schema: [
|
||||
{
|
||||
field: 'levelName',
|
||||
label: '等级',
|
||||
content: (data) => data.levelName || '-',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'experience',
|
||||
label: '成长值',
|
||||
content: (data) => data.experience || 0,
|
||||
render: (val) => val || 0,
|
||||
},
|
||||
{
|
||||
field: 'point',
|
||||
label: '当前积分',
|
||||
content: (data) => data.point || 0,
|
||||
render: (val) => val || 0,
|
||||
},
|
||||
{
|
||||
field: 'totalPoint',
|
||||
label: '总积分',
|
||||
content: (data) => data.totalPoint || 0,
|
||||
render: (val) => val || 0,
|
||||
},
|
||||
{
|
||||
field: 'balance',
|
||||
label: '当前余额',
|
||||
content: (data) => fenToYuan(data.balance || 0),
|
||||
render: (val) => fenToYuan(val || 0),
|
||||
},
|
||||
{
|
||||
field: 'totalExpense',
|
||||
label: '支出金额',
|
||||
content: (data) => fenToYuan(data.totalExpense || 0),
|
||||
render: (val) => fenToYuan(val || 0),
|
||||
},
|
||||
{
|
||||
field: 'totalRecharge',
|
||||
label: '充值金额',
|
||||
content: (data) => fenToYuan(data.totalRecharge || 0),
|
||||
render: (val) => fenToYuan(val || 0),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -77,7 +72,6 @@ const [Descriptions] = useDescription({
|
||||
<slot name="extra"></slot>
|
||||
</template>
|
||||
<Descriptions
|
||||
:column="mode === 'member' ? 2 : 1"
|
||||
:data="{
|
||||
...user,
|
||||
...wallet,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ElAvatar, ElCard, ElCol, ElRow } from 'element-plus';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{ mode?: 'kefu' | 'member'; user: MemberUserApi.User }>(),
|
||||
{
|
||||
mode: 'member',
|
||||
@@ -19,14 +19,9 @@ withDefaults(
|
||||
);
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: false,
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
border: false,
|
||||
column: props.mode === 'member' ? 2 : 1,
|
||||
labelWidth: 140,
|
||||
schema: [
|
||||
{
|
||||
field: 'name',
|
||||
@@ -43,10 +38,10 @@ const [Descriptions] = useDescription({
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
content: (data) =>
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_USER_SEX,
|
||||
value: data.sex,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -60,17 +55,17 @@ const [Descriptions] = useDescription({
|
||||
{
|
||||
field: 'birthday',
|
||||
label: '生日',
|
||||
content: (data) => formatDate(data.birthday)?.toString() || '-',
|
||||
render: (val) => formatDate(val)?.toString() || '-',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '注册时间',
|
||||
content: (data) => formatDate(data.createTime)?.toString() || '-',
|
||||
render: (val) => formatDate(val)?.toString() || '-',
|
||||
},
|
||||
{
|
||||
field: 'loginDate',
|
||||
label: '最后登录时间',
|
||||
content: (data) => formatDate(data.loginDate)?.toString() || '-',
|
||||
render: (val) => formatDate(val)?.toString() || '-',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -89,12 +84,12 @@ const [Descriptions] = useDescription({
|
||||
<ElAvatar :size="180" shape="square" :src="user.avatar" />
|
||||
</ElCol>
|
||||
<ElCol :span="18">
|
||||
<Descriptions :column="2" :data="user" />
|
||||
<Descriptions :data="user" />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<template v-else-if="mode === 'kefu'">
|
||||
<ElAvatar :size="140" shape="square" :src="user.avatar" />
|
||||
<Descriptions :column="1" :data="user" />
|
||||
<Descriptions :data="user" />
|
||||
</template>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { PayOrderApi } from '#/api/pay/order';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -184,53 +183,46 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'status',
|
||||
label: '支付状态',
|
||||
content: (data: any) =>
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.PAY_ORDER_STATUS,
|
||||
value: data?.status,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '支付金额',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
`¥${erpPriceInputFormatter(data?.price)}`,
|
||||
render: (val) => `¥${erpPriceInputFormatter(val)}`,
|
||||
},
|
||||
{
|
||||
field: 'channelFeePrice',
|
||||
label: '手续费',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
`¥${erpPriceInputFormatter(data?.channelFeePrice)}`,
|
||||
render: (val) => `¥${erpPriceInputFormatter(val)}`,
|
||||
},
|
||||
{
|
||||
field: 'channelFeeRate',
|
||||
label: '手续费比例',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
`${erpPriceInputFormatter(data?.channelFeeRate)}%`,
|
||||
render: (val) => `${erpPriceInputFormatter(val)}%`,
|
||||
},
|
||||
{
|
||||
field: 'successTime',
|
||||
label: '支付时间',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
formatDateTime(data?.successTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'expireTime',
|
||||
label: '失效时间',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
formatDateTime(data?.expireTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
formatDateTime(data?.createTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
formatDateTime(data?.updateTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'subject',
|
||||
@@ -243,10 +235,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'channelCode',
|
||||
label: '支付渠道',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.PAY_CHANNEL_CODE,
|
||||
value: data?.channelCode,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -256,10 +248,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'channelOrderNo',
|
||||
label: '渠道单号',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
data?.channelOrderNo
|
||||
? h(ElTag, { color: 'green' }, () => data.channelOrderNo)
|
||||
: '',
|
||||
render: (val) => (val ? h(ElTag, { color: 'green' }, () => val) : ''),
|
||||
},
|
||||
{
|
||||
field: 'channelUserId',
|
||||
@@ -268,8 +257,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'refundPrice',
|
||||
label: '退款金额',
|
||||
content: (data: PayOrderApi.Order) =>
|
||||
`¥${erpPriceInputFormatter(data?.refundPrice)}`,
|
||||
render: (val) => `¥${erpPriceInputFormatter(val)}`,
|
||||
},
|
||||
{
|
||||
field: 'notifyUrl',
|
||||
|
||||
@@ -13,14 +13,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<PayOrderApi.Order>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 2,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { PayRefundApi } from '#/api/pay/refund';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -173,26 +172,22 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'merchantRefundId',
|
||||
label: '商户退款单号',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
h(ElTag, {}, () => data?.merchantRefundId || '-'),
|
||||
render: (val) => h(ElTag, {}, () => val || '-'),
|
||||
},
|
||||
{
|
||||
field: 'channelRefundNo',
|
||||
label: '渠道退款单号',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
h(ElTag, { type: 'success' }, () => data?.channelRefundNo || '-'),
|
||||
render: (val) => h(ElTag, { type: 'success' }, () => val || '-'),
|
||||
},
|
||||
{
|
||||
field: 'merchantOrderId',
|
||||
label: '商户支付单号',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
h(ElTag, {}, () => data?.merchantOrderId || '-'),
|
||||
render: (val) => h(ElTag, {}, () => val || '-'),
|
||||
},
|
||||
{
|
||||
field: 'channelOrderNo',
|
||||
label: '渠道支付单号',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
h(ElTag, { type: 'success' }, () => data?.channelOrderNo || '-'),
|
||||
render: (val) => h(ElTag, { type: 'success' }, () => val || '-'),
|
||||
},
|
||||
{
|
||||
field: 'appId',
|
||||
@@ -205,58 +200,55 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'payPrice',
|
||||
label: '支付金额',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
render: (val) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: 'success' },
|
||||
() => `¥${erpPriceInputFormatter(data?.payPrice || 0)}`,
|
||||
() => `¥${erpPriceInputFormatter(val || 0)}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'refundPrice',
|
||||
label: '退款金额',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
render: (val) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: 'danger' },
|
||||
() => `¥${erpPriceInputFormatter(data?.refundPrice || 0)}`,
|
||||
() => `¥${erpPriceInputFormatter(val || 0)}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '退款状态',
|
||||
content: (data: any) =>
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.PAY_REFUND_STATUS,
|
||||
value: data?.status,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'successTime',
|
||||
label: '退款时间',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
formatDateTime(data?.successTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
formatDateTime(data?.createTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
formatDateTime(data?.updateTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
// 渠道信息部分
|
||||
{
|
||||
field: 'channelCode',
|
||||
label: '退款渠道',
|
||||
content: (data: PayRefundApi.Refund) =>
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.PAY_CHANNEL_CODE,
|
||||
value: data?.channelCode,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,14 +13,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<PayRefundApi.Refund>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 2,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { PayTransferApi } from '#/api/pay/transfer';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -191,14 +190,12 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'merchantTransferId',
|
||||
label: '商户单号',
|
||||
content: (data: PayTransferApi.Transfer) =>
|
||||
h(ElTag, {}, () => data?.merchantTransferId),
|
||||
render: (val) => h(ElTag, {}, () => val),
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
label: '转账单号',
|
||||
content: (data: PayTransferApi.Transfer) =>
|
||||
h(ElTag, { color: 'orange' }, () => data?.no),
|
||||
render: (val) => h(ElTag, { color: 'orange' }, () => val),
|
||||
},
|
||||
{
|
||||
field: 'appId',
|
||||
@@ -207,33 +204,31 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'status',
|
||||
label: '转账状态',
|
||||
content: (data: any) =>
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.PAY_TRANSFER_STATUS,
|
||||
value: data?.status,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '转账金额',
|
||||
content: (data: PayTransferApi.Transfer) =>
|
||||
render: (val) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ color: 'success' },
|
||||
() => `¥${erpPriceInputFormatter(data?.price || 0)}`,
|
||||
() => `¥${erpPriceInputFormatter(val || 0)}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'successTime',
|
||||
label: '转账时间',
|
||||
content: (data: PayTransferApi.Transfer) =>
|
||||
formatDateTime(data?.successTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: PayTransferApi.Transfer) =>
|
||||
formatDateTime(data?.createTime) as string,
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
@@ -246,10 +241,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'channelCode',
|
||||
label: '支付渠道',
|
||||
content: (data: PayTransferApi.Transfer) =>
|
||||
render: (val) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.PAY_CHANNEL_CODE,
|
||||
value: data?.channelCode,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -259,10 +254,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'channelTransferNo',
|
||||
label: '渠道单号',
|
||||
content: (data: PayTransferApi.Transfer) =>
|
||||
data?.channelTransferNo
|
||||
? h(ElTag, { color: 'success' }, () => data.channelTransferNo)
|
||||
: '',
|
||||
render: (val) => (val ? h(ElTag, { color: 'success' }, () => val) : ''),
|
||||
},
|
||||
{
|
||||
field: 'notifyUrl',
|
||||
|
||||
@@ -13,14 +13,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<PayTransferApi.Transfer>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 2,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -110,10 +109,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'logType',
|
||||
label: '操作类型',
|
||||
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_LOGIN_TYPE,
|
||||
value: data?.logType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -132,19 +131,17 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'result',
|
||||
label: '登录结果',
|
||||
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_LOGIN_RESULT,
|
||||
value: data?.result,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '登录日期',
|
||||
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,14 +12,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemLoginLogApi.LoginLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 110,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 110,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -164,9 +163,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'fromMail',
|
||||
@@ -175,14 +172,14 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userId',
|
||||
label: '接收用户',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
if (data?.userType && data?.userId) {
|
||||
render: (val, data) => {
|
||||
if (data?.userType && val) {
|
||||
return h('div', [
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
}),
|
||||
` (${data.userId})`,
|
||||
` (${val})`,
|
||||
]);
|
||||
}
|
||||
return '无';
|
||||
@@ -191,10 +188,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'toMails',
|
||||
label: '接收信息',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
render: (val, data) => {
|
||||
const lines: string[] = [];
|
||||
if (data?.toMails && data.toMails.length > 0) {
|
||||
lines.push(`收件:${data.toMails.join('、')}`);
|
||||
if (val && val.length > 0) {
|
||||
lines.push(`收件:${val.join('、')}`);
|
||||
}
|
||||
if (data?.ccMails && data.ccMails.length > 0) {
|
||||
lines.push(`抄送:${data.ccMails.join('、')}`);
|
||||
@@ -227,28 +224,26 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
field: 'templateContent',
|
||||
label: '邮件内容',
|
||||
span: 2,
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
render: (val) => {
|
||||
return h('div', {
|
||||
innerHTML: data?.templateContent || '',
|
||||
innerHTML: val || '',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sendStatus',
|
||||
label: '发送状态',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS,
|
||||
value: data?.sendStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sendTime',
|
||||
label: '发送时间',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
return formatDateTime(data?.sendTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'sendMessageId',
|
||||
|
||||
@@ -12,14 +12,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemMailLogApi.MailLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 2,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -166,10 +165,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data?.userType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -196,9 +195,9 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'templateParams',
|
||||
label: '模版参数',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
try {
|
||||
return JSON.stringify(data?.templateParams);
|
||||
return JSON.stringify(val);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -207,36 +206,32 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'templateType',
|
||||
label: '模版类型',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
value: data?.templateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readStatus',
|
||||
label: '是否已读',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: data?.readStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readTime',
|
||||
label: '阅读时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.readTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,14 +12,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -103,36 +102,32 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '发送时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'templateType',
|
||||
label: '消息类型',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
value: data?.templateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readStatus',
|
||||
label: '是否已读',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: data?.readStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readTime',
|
||||
label: '阅读时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.readTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'templateContent',
|
||||
|
||||
@@ -12,14 +12,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
@@ -134,7 +133,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'traceId',
|
||||
label: '链路追踪',
|
||||
hidden: (data: SystemOperateLogApi.OperateLog) => !data?.traceId,
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
@@ -167,13 +166,14 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'extra',
|
||||
label: '操作拓展参数',
|
||||
hidden: (data: SystemOperateLogApi.OperateLog) => !data?.extra,
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
label: '请求 URL',
|
||||
content: (data: SystemOperateLogApi.OperateLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
field: 'requestUrl',
|
||||
render: (val, data) => {
|
||||
if (data?.requestMethod && val) {
|
||||
return `${data.requestMethod} ${val}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -181,9 +181,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '操作时间',
|
||||
content: (data: SystemOperateLogApi.OperateLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'bizId',
|
||||
|
||||
@@ -12,14 +12,9 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemOperateLogApi.OperateLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 110,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 110,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemSmsLogApi } from '#/api/system/sms/log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -179,9 +178,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
@@ -198,10 +195,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'templateType',
|
||||
label: '模板类型',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
|
||||
value: data?.templateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -212,19 +209,17 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'sendStatus',
|
||||
label: '发送状态',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SMS_SEND_STATUS,
|
||||
value: data?.sendStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sendTime',
|
||||
label: '发送时间',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
return formatDateTime(data?.sendTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'apiSendCode',
|
||||
@@ -237,19 +232,17 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'receiveStatus',
|
||||
label: '接收状态',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS,
|
||||
value: data?.receiveStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'receiveTime',
|
||||
label: '接收时间',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
return formatDateTime(data?.receiveTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'apiReceiveCode',
|
||||
|
||||
@@ -12,14 +12,9 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemSmsLogApi.SmsLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
title: '',
|
||||
extra: '',
|
||||
},
|
||||
column: 2,
|
||||
direction: 'horizontal',
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemSocialUserApi } from '#/api/system/social/user';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -8,6 +7,8 @@ import { h } from 'vue';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
@@ -111,10 +112,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'type',
|
||||
label: '社交平台',
|
||||
content: (data: SystemSocialUserApi.SocialUser) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SOCIAL_TYPE,
|
||||
value: data?.type,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -125,16 +126,13 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'avatar',
|
||||
label: '用户头像',
|
||||
// TODO @芋艿:使用 antd 的 Image 组件
|
||||
content: (data: SystemSocialUserApi.SocialUser) => {
|
||||
if (data?.avatar) {
|
||||
return h('img', {
|
||||
src: data.avatar,
|
||||
style: 'width: 30px; height: 30px; cursor: pointer;',
|
||||
onClick: () => {
|
||||
// 可以添加图片预览功能
|
||||
window.open(data.avatar, '_blank');
|
||||
},
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(ElImage, {
|
||||
src: val,
|
||||
previewSrcList: [val],
|
||||
class: 'w-10 h-10 cursor-pointer',
|
||||
previewTeleported: true,
|
||||
});
|
||||
}
|
||||
return '无';
|
||||
|
||||
@@ -14,14 +14,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemSocialUserApi.SocialUser>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
border: true,
|
||||
column: 1,
|
||||
direction: 'horizontal',
|
||||
title: '',
|
||||
extra: '',
|
||||
labelWidth: 185,
|
||||
},
|
||||
column: 1,
|
||||
labelWidth: 185,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,80 +1,198 @@
|
||||
<script lang="tsx">
|
||||
import type { DescriptionsProps } from 'naive-ui';
|
||||
import type { DescriptionsProps as NDescriptionsProps } from 'naive-ui';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
import type { CSSProperties, PropType, Slots } from 'vue';
|
||||
|
||||
import type { DescriptionItemSchema, DescriptionsOptions } from './typing';
|
||||
import type { DescriptionItemSchema, DescriptionProps } from './typing';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import { computed, defineComponent, ref, unref, useAttrs } from 'vue';
|
||||
|
||||
import { get } from '@vben/utils';
|
||||
import { get, getNestedValue, isFunction } from '@vben/utils';
|
||||
|
||||
import { NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
/** 对 Descriptions 进行二次封装 */
|
||||
const Description = defineComponent({
|
||||
name: 'Descriptions',
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
schema: {
|
||||
type: Array as PropType<DescriptionItemSchema[]>,
|
||||
default: () => [],
|
||||
},
|
||||
// Descriptions 原生 props
|
||||
componentProps: {
|
||||
type: Object as PropType<DescriptionsProps>,
|
||||
default: () => ({}),
|
||||
const props = {
|
||||
bordered: { default: true, type: Boolean },
|
||||
column: {
|
||||
default: () => {
|
||||
return { lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4 };
|
||||
},
|
||||
type: [Number, Object],
|
||||
},
|
||||
data: { type: Object },
|
||||
schema: {
|
||||
default: () => [],
|
||||
type: Array as PropType<DescriptionItemSchema[]>,
|
||||
},
|
||||
size: {
|
||||
default: 'small',
|
||||
type: String,
|
||||
validator: (v: string) =>
|
||||
['default', 'middle', 'small', undefined].includes(v),
|
||||
},
|
||||
title: { default: '', type: String },
|
||||
useCard: { default: true, type: Boolean },
|
||||
labelPlacement: { default: 'left', type: String as PropType<'left' | 'top'> },
|
||||
};
|
||||
|
||||
setup(props: DescriptionsOptions) {
|
||||
// TODO @xingyu:每个 field 的 slot 的考虑
|
||||
// TODO @xingyu:from 5.0:extra: () => getSlot(slots, 'extra')
|
||||
/** 过滤掉不需要展示的 */
|
||||
const shouldShowItem = (item: DescriptionItemSchema) => {
|
||||
if (item.hidden === undefined) return true;
|
||||
return typeof item.hidden === 'function'
|
||||
? !item.hidden(props.data)
|
||||
: !item.hidden;
|
||||
};
|
||||
/** 渲染内容 */
|
||||
const renderContent = (item: DescriptionItemSchema) => {
|
||||
if (item.content) {
|
||||
return typeof item.content === 'function'
|
||||
? item.content(props.data)
|
||||
: item.content;
|
||||
function getSlot(slots: Slots, slot: string, data?: any) {
|
||||
if (!slots || !Reflect.has(slots, slot)) {
|
||||
return null;
|
||||
}
|
||||
if (!isFunction(slots[slot])) {
|
||||
console.error(`${slot} is not a function!`);
|
||||
return null;
|
||||
}
|
||||
const slotFn = slots[slot];
|
||||
if (!slotFn) return null;
|
||||
return slotFn({ data });
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Description',
|
||||
props,
|
||||
setup(props, { slots }) {
|
||||
const propsRef = ref<null | Partial<DescriptionProps>>(null);
|
||||
|
||||
const prefixCls = 'description';
|
||||
const attrs = useAttrs();
|
||||
|
||||
// Custom title component: get title
|
||||
const getMergeProps = computed(() => {
|
||||
return {
|
||||
...props,
|
||||
...(unref(propsRef) as any),
|
||||
} as DescriptionProps;
|
||||
});
|
||||
|
||||
const getProps = computed(() => {
|
||||
const opt = {
|
||||
...unref(getMergeProps),
|
||||
title: undefined,
|
||||
};
|
||||
return opt as DescriptionProps;
|
||||
});
|
||||
|
||||
const useWrapper = computed(() => !!unref(getMergeProps).title);
|
||||
|
||||
const getDescriptionsProps = computed(() => {
|
||||
return { ...unref(attrs), ...unref(getProps) } as NDescriptionsProps;
|
||||
});
|
||||
|
||||
// 防止换行
|
||||
function renderLabel({
|
||||
label,
|
||||
labelMinWidth,
|
||||
labelStyle,
|
||||
}: DescriptionItemSchema) {
|
||||
if (!labelStyle && !labelMinWidth) {
|
||||
return label;
|
||||
}
|
||||
return item.field ? get(props.data, item.field) : null;
|
||||
};
|
||||
|
||||
return () => (
|
||||
<NDescriptions
|
||||
{...props}
|
||||
bordered={props.componentProps?.bordered}
|
||||
column={props.componentProps?.column}
|
||||
labelPlacement={props.componentProps?.labelPlacement || 'left'}
|
||||
size={props.componentProps?.size}
|
||||
title={props.componentProps?.title}
|
||||
>
|
||||
{props.schema?.filter(shouldShowItem).map((item) => (
|
||||
<NDescriptionsItem
|
||||
contentStyle={item.contentStyle}
|
||||
key={item.field || String(item.label)}
|
||||
label={item.label as string}
|
||||
labelStyle={item.labelStyle}
|
||||
span={item.span}
|
||||
>
|
||||
{renderContent(item)}
|
||||
</NDescriptionsItem>
|
||||
))}
|
||||
</NDescriptions>
|
||||
);
|
||||
const labelStyles: CSSProperties = {
|
||||
...labelStyle,
|
||||
minWidth: `${labelMinWidth}px `,
|
||||
};
|
||||
return <div style={labelStyles}>{label}</div>;
|
||||
}
|
||||
|
||||
function renderItem() {
|
||||
const { data, schema } = unref(getProps);
|
||||
return unref(schema)
|
||||
.map((item) => {
|
||||
const { contentMinWidth, field, render, show, span } = item;
|
||||
|
||||
if (show && isFunction(show) && !show(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
const _data = unref(getProps)?.data;
|
||||
if (!_data) {
|
||||
return null;
|
||||
}
|
||||
const getField = field.includes('.')
|
||||
? (getNestedValue(_data, field) ?? get(_data, field))
|
||||
: get(_data, field);
|
||||
// if (
|
||||
// getField &&
|
||||
// !Object.prototype.hasOwnProperty.call(toRefs(_data), field)
|
||||
// ) {
|
||||
// return isFunction(render) ? render('', _data) : (getField ?? '');
|
||||
// }
|
||||
return isFunction(render)
|
||||
? render(getField, _data)
|
||||
: (getField ?? '');
|
||||
}
|
||||
|
||||
const width = contentMinWidth;
|
||||
return (
|
||||
<NDescriptionsItem key={field} span={span}>
|
||||
{{
|
||||
label: () => {
|
||||
return renderLabel(item);
|
||||
},
|
||||
default: () => {
|
||||
if (item.slot) {
|
||||
const slotContent = getSlot(slots, item.slot, data);
|
||||
return slotContent;
|
||||
}
|
||||
if (!contentMinWidth) {
|
||||
return getContent();
|
||||
}
|
||||
const style: CSSProperties = {
|
||||
minWidth: `${width}px`,
|
||||
};
|
||||
return <div style={style}>{getContent()}</div>;
|
||||
},
|
||||
}}
|
||||
</NDescriptionsItem>
|
||||
);
|
||||
})
|
||||
.filter((item) => !!item);
|
||||
}
|
||||
|
||||
function renderDesc() {
|
||||
return (
|
||||
<NDescriptions
|
||||
class={`${prefixCls}`}
|
||||
{...(unref(getDescriptionsProps) as any)}
|
||||
>
|
||||
{renderItem()}
|
||||
</NDescriptions>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCard() {
|
||||
const content = props.useCard ? renderDesc() : <div>{renderDesc()}</div>;
|
||||
// Reduce the dom level
|
||||
if (!props.useCard) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const { title } = unref(getMergeProps);
|
||||
const extraSlot = getSlot(slots, 'extra');
|
||||
|
||||
return (
|
||||
<NCard
|
||||
contentStyle={{ padding: '8px 0' }}
|
||||
headerStyle={{
|
||||
padding: '8px 16px',
|
||||
fontSize: '14px',
|
||||
minHeight: '24px',
|
||||
}}
|
||||
style={{ margin: 0 }}
|
||||
title={title}
|
||||
>
|
||||
{{
|
||||
default: () => content,
|
||||
extra: () => extraSlot && <div>{extraSlot}</div>,
|
||||
}}
|
||||
</NCard>
|
||||
);
|
||||
}
|
||||
|
||||
return () => (unref(useWrapper) ? renderCard() : renderDesc());
|
||||
},
|
||||
});
|
||||
|
||||
// TODO @xingyu:from 5.0:emits: ['register'] 事件
|
||||
export default Description;
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import type { DescriptionsProps } from 'naive-ui';
|
||||
import type { DescriptionsProps as NDescriptionsProps } from 'naive-ui';
|
||||
import type { JSX } from 'vue/jsx-runtime';
|
||||
|
||||
import type { CSSProperties, VNode } from 'vue';
|
||||
|
||||
// TODO @xingyu:【content】这个纠结下;1)vben2.0 是 render;https://doc.vvbin.cn/components/desc.html#usage 2)
|
||||
// TODO @xingyu:vben2.0 还有 sapn【done】、labelMinWidth、contentMinWidth
|
||||
// TODO @xingyu:【hidden】这个纠结下;1)vben2.0 是 show;
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
export interface DescriptionItemSchema {
|
||||
label: string | VNode; // 内容的描述
|
||||
field?: string; // 对应 data 中的字段名
|
||||
content?: ((data: any) => string | VNode) | string | VNode; // 自定义需要展示的内容,比如说 dict-tag
|
||||
span?: number; // 包含列的数量
|
||||
labelStyle?: CSSProperties; // 自定义标签样式
|
||||
contentStyle?: CSSProperties; // 自定义内容样式
|
||||
hidden?: ((data: any) => boolean) | boolean; // 是否显示
|
||||
labelMinWidth?: number;
|
||||
contentMinWidth?: number;
|
||||
// 自定义标签样式
|
||||
labelStyle?: CSSProperties;
|
||||
// 对应 data 中的字段名
|
||||
field: string;
|
||||
// 内容的描述
|
||||
label: JSX.Element | string | VNode;
|
||||
// 包含列的数量
|
||||
span?: number;
|
||||
// 是否显示
|
||||
show?: (...arg: any) => boolean;
|
||||
// 插槽名称
|
||||
slot?: string;
|
||||
// 自定义需要展示的内容
|
||||
render?: (
|
||||
val: any,
|
||||
data?: Recordable<any>,
|
||||
) => Element | JSX.Element | number | string | undefined | VNode;
|
||||
}
|
||||
|
||||
// TODO @xingyu:vben2.0 还有 title【done】、bordered【done】d、useCollapse、collapseOptions
|
||||
// TODO @xingyu:from 5.0:bordered 默认为 true
|
||||
// TODO @xingyu:from 5.0:column 默认为 lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4
|
||||
// TODO @xingyu:from 5.0:size 默认为 small;有 'default', 'middle', 'small', undefined
|
||||
// TODO @xingyu:from 5.0:useCollapse 默认为 true
|
||||
export interface DescriptionsOptions {
|
||||
data?: Record<string, any>; // 数据
|
||||
schema?: DescriptionItemSchema[]; // 描述项配置
|
||||
componentProps?: DescriptionsProps; // antd Descriptions 组件参数
|
||||
export interface DescriptionProps extends NDescriptionsProps {
|
||||
// 是否包含卡片组件
|
||||
useCard?: boolean;
|
||||
// 描述项配置
|
||||
schema: DescriptionItemSchema[];
|
||||
// 数据
|
||||
data: Recordable<any>;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
}
|
||||
|
||||
@@ -1,71 +1,31 @@
|
||||
import type { DescriptionsOptions } from './typing';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { defineComponent, h, isReactive, reactive, watch } from 'vue';
|
||||
import type { DescInstance, DescriptionProps } from './typing';
|
||||
|
||||
import { h, reactive } from 'vue';
|
||||
|
||||
import Description from './description.vue';
|
||||
|
||||
/** 描述列表 api 定义 */
|
||||
class DescriptionApi {
|
||||
private state = reactive<Record<string, any>>({});
|
||||
export function useDescription(options?: Partial<DescriptionProps>) {
|
||||
const propsState = reactive<Partial<DescriptionProps>>(options || {});
|
||||
|
||||
constructor(options: DescriptionsOptions) {
|
||||
this.state = { ...options };
|
||||
}
|
||||
const api: DescInstance = {
|
||||
setDescProps: (descProps: Partial<DescriptionProps>): void => {
|
||||
Object.assign(propsState, descProps);
|
||||
},
|
||||
};
|
||||
|
||||
getState(): DescriptionsOptions {
|
||||
return this.state as DescriptionsOptions;
|
||||
}
|
||||
|
||||
// TODO @xingyu:【setState】纠结下:1)vben2.0 是 data https://doc.vvbin.cn/components/desc.html#usage;
|
||||
setState(newState: Partial<DescriptionsOptions>) {
|
||||
this.state = { ...this.state, ...newState };
|
||||
}
|
||||
}
|
||||
|
||||
export type ExtendedDescriptionApi = DescriptionApi;
|
||||
|
||||
export function useDescription(options: DescriptionsOptions) {
|
||||
const IS_REACTIVE = isReactive(options);
|
||||
const api = new DescriptionApi(options);
|
||||
// 扩展 API
|
||||
const extendedApi: ExtendedDescriptionApi = api as never;
|
||||
const Desc = defineComponent({
|
||||
// 创建一个包装组件,将 propsState 合并到 props 中
|
||||
const DescriptionWrapper: Component = {
|
||||
name: 'UseDescription',
|
||||
inheritAttrs: false,
|
||||
setup(_, { attrs, slots }) {
|
||||
// 合并props和attrs到state
|
||||
api.setState({ ...attrs });
|
||||
|
||||
return () =>
|
||||
h(
|
||||
Description,
|
||||
{
|
||||
...api.getState(),
|
||||
...attrs,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => {
|
||||
// @ts-ignore - 避免类型实例化过深
|
||||
return h(Description, { ...propsState, ...attrs }, slots);
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 响应式支持
|
||||
if (IS_REACTIVE) {
|
||||
watch(
|
||||
() => options.schema,
|
||||
(newSchema) => {
|
||||
api.setState({ schema: newSchema });
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => options.data,
|
||||
(newData) => {
|
||||
api.setState({ data: newData });
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
}
|
||||
|
||||
return [Desc, extendedApi] as const;
|
||||
return [DescriptionWrapper, api] as const;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -181,10 +180,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -197,10 +196,11 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
label: '用户 UA',
|
||||
},
|
||||
{
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
render: (val, data) => {
|
||||
if (val && data?.requestUrl) {
|
||||
return `${val} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -208,10 +208,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data.requestParams) {
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(data.requestParams),
|
||||
value: JSON.parse(val),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
@@ -224,26 +224,29 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
},
|
||||
{
|
||||
label: '请求时间',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.beginTime && data?.endTime) {
|
||||
return `${formatDateTime(data.beginTime)} ~ ${formatDateTime(data.endTime)}`;
|
||||
field: 'beginTime',
|
||||
render: (val, data) => {
|
||||
if (val && data?.endTime) {
|
||||
return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '请求耗时',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
return data?.duration ? `${data.duration} ms` : '';
|
||||
field: 'duration',
|
||||
render: (val) => {
|
||||
return val ? `${val} ms` : '';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '操作结果',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
if (data?.resultCode === 0) {
|
||||
field: 'resultCode',
|
||||
render: (val, data) => {
|
||||
if (val === 0) {
|
||||
return '正常';
|
||||
} else if (data && data.resultCode > 0) {
|
||||
return `失败 | ${data.resultCode} | ${data.resultMsg}`;
|
||||
} else if (val > 0 && data?.resultMsg) {
|
||||
return `失败 | ${val} | ${data.resultMsg}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -259,10 +262,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'operateType',
|
||||
label: '操作类型',
|
||||
content: (data: InfraApiAccessLogApi.ApiAccessLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_OPERATE_TYPE,
|
||||
value: data?.operateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -158,10 +157,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -176,9 +175,9 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'requestMethod',
|
||||
label: '请求信息',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
render: (val, data) => {
|
||||
if (val && data?.requestUrl) {
|
||||
return `${val} ${data.requestUrl}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -186,10 +185,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data.requestParams) {
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(JsonViewer, {
|
||||
value: JSON.parse(data.requestParams),
|
||||
value: JSON.parse(val),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
@@ -199,8 +198,8 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'exceptionTime',
|
||||
label: '异常时间',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return formatDateTime(data?.exceptionTime || '') as string;
|
||||
render: (val) => {
|
||||
return formatDateTime(val) as string;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -210,12 +209,11 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'exceptionStackTrace',
|
||||
label: '异常堆栈',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) =>
|
||||
!data?.exceptionStackTrace,
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
if (data?.exceptionStackTrace) {
|
||||
show: (val) => !val,
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h('textarea', {
|
||||
value: data.exceptionStackTrace,
|
||||
value: val,
|
||||
style:
|
||||
'width: 100%; min-height: 200px; max-height: 400px; resize: vertical;',
|
||||
readonly: true,
|
||||
@@ -227,24 +225,24 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'processStatus',
|
||||
label: '处理状态',
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
|
||||
value: data?.processStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'processUserId',
|
||||
label: '处理人',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) => !data?.processUserId,
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
field: 'processTime',
|
||||
label: '处理时间',
|
||||
hidden: (data: InfraApiErrorLogApi.ApiErrorLog) => !data?.processTime,
|
||||
content: (data: InfraApiErrorLogApi.ApiErrorLog) => {
|
||||
return formatDateTime(data?.processTime || '') as string;
|
||||
show: (val) => !val,
|
||||
render: (val) => {
|
||||
return formatDateTime(val) as string;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobApi } from '#/api/infra/job';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
@@ -191,10 +190,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'status',
|
||||
label: '任务状态',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_JOB_STATUS,
|
||||
value: data?.status,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -216,27 +215,27 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
},
|
||||
{
|
||||
label: '重试间隔',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
return data?.retryInterval ? `${data.retryInterval} 毫秒` : '无间隔';
|
||||
field: 'retryInterval',
|
||||
render: (val) => {
|
||||
return val ? `${val} 毫秒` : '无间隔';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '监控超时时间',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
return data?.monitorTimeout && data.monitorTimeout > 0
|
||||
? `${data.monitorTimeout} 毫秒`
|
||||
: '未开启';
|
||||
field: 'monitorTimeout',
|
||||
render: (val) => {
|
||||
return val && val > 0 ? `${val} 毫秒` : '未开启';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nextTimes',
|
||||
label: '后续执行时间',
|
||||
content: (data: InfraJobApi.Job) => {
|
||||
if (!data?.nextTimes || data.nextTimes.length === 0) {
|
||||
render: (val) => {
|
||||
if (!val || val.length === 0) {
|
||||
return '无后续执行时间';
|
||||
}
|
||||
return h(NTimeline, {}, () =>
|
||||
data.nextTimes?.map((time: Date) =>
|
||||
val?.map((time: Date) =>
|
||||
h(NTimelineItem, {}, () => formatDateTime(time)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { InfraJobLogApi } from '#/api/infra/job-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -148,9 +147,9 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'beginTime',
|
||||
label: '执行时间',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
if (data?.beginTime && data?.endTime) {
|
||||
return `${formatDateTime(data.beginTime)} ~ ${formatDateTime(data.endTime)}`;
|
||||
render: (val, data) => {
|
||||
if (val && data?.endTime) {
|
||||
return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -158,17 +157,15 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'duration',
|
||||
label: '执行时长',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
return data?.duration ? `${data.duration} 毫秒` : '';
|
||||
},
|
||||
render: (val) => (val ? `${val} 毫秒` : ''),
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '任务状态',
|
||||
content: (data: InfraJobLogApi.JobLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_JOB_LOG_STATUS,
|
||||
value: data?.status,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,11 +13,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<InfraJobLogApi.JobLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,11 +14,7 @@ const formData = ref<InfraJobApi.Job>(); // 任务详情
|
||||
const nextTimes = ref<Date[]>([]); // 下一次执行时间
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ function renderMemoryChart() {
|
||||
detail: {
|
||||
show: true,
|
||||
offsetCenter: [0, '50%'],
|
||||
color: 'auto',
|
||||
color: 'inherit',
|
||||
fontSize: 30,
|
||||
formatter: usedMemory,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemLoginLogApi } from '#/api/system/login-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -110,10 +109,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'logType',
|
||||
label: '操作类型',
|
||||
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_LOGIN_TYPE,
|
||||
value: data?.logType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -132,19 +131,17 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'result',
|
||||
label: '登录结果',
|
||||
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_LOGIN_RESULT,
|
||||
value: data?.result,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '登录日期',
|
||||
content: (data: SystemLoginLogApi.LoginLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemLoginLogApi.LoginLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemMailLogApi } from '#/api/system/mail/log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -164,9 +163,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'fromMail',
|
||||
@@ -175,12 +172,12 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userId',
|
||||
label: '接收用户',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
if (data?.userType && data?.userId) {
|
||||
render: (val, data) => {
|
||||
if (val && data?.userId) {
|
||||
return h('div', [
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data.userType,
|
||||
value: val,
|
||||
}),
|
||||
` (${data.userId})`,
|
||||
]);
|
||||
@@ -191,10 +188,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'toMails',
|
||||
label: '接收信息',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
render: (val, data) => {
|
||||
const lines: string[] = [];
|
||||
if (data?.toMails && data.toMails.length > 0) {
|
||||
lines.push(`收件:${data.toMails.join('、')}`);
|
||||
if (val && val.length > 0) {
|
||||
lines.push(`收件:${val.join('、')}`);
|
||||
}
|
||||
if (data?.ccMails && data.ccMails.length > 0) {
|
||||
lines.push(`抄送:${data.ccMails.join('、')}`);
|
||||
@@ -227,28 +224,22 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
field: 'templateContent',
|
||||
label: '邮件内容',
|
||||
span: 2,
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
return h('div', {
|
||||
innerHTML: data?.templateContent || '',
|
||||
});
|
||||
},
|
||||
render: (val) => h('div', { innerHTML: val || '' }),
|
||||
},
|
||||
{
|
||||
field: 'sendStatus',
|
||||
label: '发送状态',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_MAIL_SEND_STATUS,
|
||||
value: data?.sendStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sendTime',
|
||||
label: '发送时间',
|
||||
content: (data: SystemMailLogApi.MailLog) => {
|
||||
return formatDateTime(data?.sendTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'sendMessageId',
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemMailLogApi.MailLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 2,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 2,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -166,10 +165,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'userType',
|
||||
label: '用户类型',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.USER_TYPE,
|
||||
value: data?.userType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -196,9 +195,9 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'templateParams',
|
||||
label: '模版参数',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
try {
|
||||
return JSON.stringify(data?.templateParams);
|
||||
return JSON.stringify(val);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -207,36 +206,32 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'templateType',
|
||||
label: '模版类型',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
value: data?.templateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readStatus',
|
||||
label: '是否已读',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: data?.readStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readTime',
|
||||
label: '阅读时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.readTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemNotifyMessageApi } from '#/api/system/notify/message';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -103,36 +102,32 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '发送时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'templateType',
|
||||
label: '消息类型',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE,
|
||||
value: data?.templateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readStatus',
|
||||
label: '是否已读',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: data?.readStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'readTime',
|
||||
label: '阅读时间',
|
||||
content: (data: SystemNotifyMessageApi.NotifyMessage) => {
|
||||
return formatDateTime(data?.readTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'templateContent',
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemNotifyMessageApi.NotifyMessage>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
@@ -134,7 +133,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'traceId',
|
||||
label: '链路追踪',
|
||||
hidden: (data: SystemOperateLogApi.OperateLog) => !data?.traceId,
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
@@ -167,13 +166,14 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'extra',
|
||||
label: '操作拓展参数',
|
||||
hidden: (data: SystemOperateLogApi.OperateLog) => !data?.extra,
|
||||
show: (val) => !val,
|
||||
},
|
||||
{
|
||||
field: 'requestUrl',
|
||||
label: '请求 URL',
|
||||
content: (data: SystemOperateLogApi.OperateLog) => {
|
||||
if (data?.requestMethod && data?.requestUrl) {
|
||||
return `${data.requestMethod} ${data.requestUrl}`;
|
||||
render: (val, data) => {
|
||||
if (data?.requestMethod && val) {
|
||||
return `${data.requestMethod} ${val}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@@ -181,9 +181,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '操作时间',
|
||||
content: (data: SystemOperateLogApi.OperateLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'bizId',
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemOperateLogApi.OperateLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 1,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemSmsLogApi } from '#/api/system/sms/log';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -179,9 +178,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
return formatDateTime(data?.createTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
@@ -198,10 +195,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'templateType',
|
||||
label: '模板类型',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE,
|
||||
value: data?.templateType,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -212,19 +209,17 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'sendStatus',
|
||||
label: '发送状态',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SMS_SEND_STATUS,
|
||||
value: data?.sendStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sendTime',
|
||||
label: '发送时间',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
return formatDateTime(data?.sendTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'apiSendCode',
|
||||
@@ -237,19 +232,17 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'receiveStatus',
|
||||
label: '接收状态',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS,
|
||||
value: data?.receiveStatus,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'receiveTime',
|
||||
label: '接收时间',
|
||||
content: (data: SystemSmsLogApi.SmsLog) => {
|
||||
return formatDateTime(data?.receiveTime || '') as string;
|
||||
},
|
||||
render: (val) => formatDateTime(val) as string,
|
||||
},
|
||||
{
|
||||
field: 'apiReceiveCode',
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemSmsLogApi.SmsLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 2,
|
||||
contentClass: 'mx-4',
|
||||
},
|
||||
column: 2,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemSocialUserApi } from '#/api/system/social/user';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -8,6 +7,8 @@ import { h } from 'vue';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { NImage } from 'naive-ui';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
@@ -111,10 +112,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'type',
|
||||
label: '社交平台',
|
||||
content: (data: SystemSocialUserApi.SocialUser) => {
|
||||
render: (val) => {
|
||||
return h(DictTag, {
|
||||
type: DICT_TYPE.SYSTEM_SOCIAL_TYPE,
|
||||
value: data?.type,
|
||||
value: val,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -125,16 +126,11 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
{
|
||||
field: 'avatar',
|
||||
label: '用户头像',
|
||||
// TODO @芋艿:使用 antd 的 Image 组件
|
||||
content: (data: SystemSocialUserApi.SocialUser) => {
|
||||
if (data?.avatar) {
|
||||
return h('img', {
|
||||
src: data.avatar,
|
||||
render: (val) => {
|
||||
if (val) {
|
||||
return h(NImage, {
|
||||
src: val,
|
||||
style: 'width: 30px; height: 30px; cursor: pointer;',
|
||||
onClick: () => {
|
||||
// 可以添加图片预览功能
|
||||
window.open(data.avatar, '_blank');
|
||||
},
|
||||
});
|
||||
}
|
||||
return '无';
|
||||
|
||||
@@ -14,13 +14,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<SystemSocialUserApi.SocialUser>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
componentProps: {
|
||||
bordered: true,
|
||||
column: 1,
|
||||
size: 'middle',
|
||||
class: 'mx-4',
|
||||
labelStyle: { width: '185px' },
|
||||
},
|
||||
column: 1,
|
||||
size: 'medium',
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -48,23 +48,42 @@ export function getNestedValue<T>(obj: T, path: string): any {
|
||||
* @param key 参数键名
|
||||
* @param urlStr 链接地址,默认为当前浏览器的地址
|
||||
*/
|
||||
export const getUrlNumberValue = (
|
||||
export function getUrlNumberValue(
|
||||
key: string,
|
||||
urlStr: string = location.href,
|
||||
): number => {
|
||||
): number {
|
||||
return Number(getUrlValue(key, urlStr));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取链接的参数值
|
||||
* @param key 参数键名
|
||||
* @param urlStr 链接地址,默认为当前浏览器的地址
|
||||
*/
|
||||
export const getUrlValue = (
|
||||
export function getUrlValue(
|
||||
key: string,
|
||||
urlStr: string = location.href,
|
||||
): string => {
|
||||
): string {
|
||||
if (!urlStr || !key) return '';
|
||||
const url = new URL(decodeURIComponent(urlStr));
|
||||
return url.searchParams.get(key) ?? '';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2}
|
||||
* @param target 目标对象
|
||||
* @param source 源对象
|
||||
*/
|
||||
export function copyValueToTarget(target: any, source: any) {
|
||||
const newObj = Object.assign({}, target, source);
|
||||
// 删除多余属性
|
||||
Object.keys(newObj).forEach((key) => {
|
||||
// 如果不是target中的属性则删除
|
||||
if (!Object.keys(target).includes(key)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete newObj[key];
|
||||
}
|
||||
});
|
||||
// 更新目标对象值
|
||||
Object.assign(target, newObj);
|
||||
}
|
||||
|
||||
1739
pnpm-lock.yaml
generated
1739
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user