feat(cms): 新增网站密钥和字段加密功能- 在 CMS 网站模型中新增 websiteSecret 字段用于存储小程序密钥

- 为 CMS 网站字段添加 encrypted 字段以支持内容加密- 实现字段值的加密和解密功能,提升数据安全性- 更新网站编辑组件以支持密钥的输入和存储- 调整字段编辑组件,增加加密开关和类型选项
-优化字段列表展示逻辑,支持加密字段的特殊处理- 修改字段值类型为 any以适应多种数据格式
- 完善字段编辑表单验证规则和保存逻辑
- 更新相关组件的类型定义和事件处理- 调整界面布局和交互细节,提升用户体验
This commit is contained in:
2025-09-30 17:25:23 +08:00
parent cdb90cb60f
commit db143a73d6
15 changed files with 1528 additions and 300 deletions

View File

@@ -1,5 +1,5 @@
VITE_APP_NAME=后台管理(开发环境)
#VITE_API_URL=http://127.0.0.1:9200/api
VITE_API_URL=http://127.0.0.1:9200/api
#VITE_SERVER_API_URL=http://127.0.0.1:8000/api

View File

@@ -12,6 +12,8 @@ export interface CmsWebsite {
websiteName?: string;
// 网站标识
websiteCode?: string;
// 网站密钥
websiteSecret?: string;
// 网站LOGO
websiteIcon?: string;
// 网站LOGO

View File

@@ -19,9 +19,11 @@ export interface CmsWebsiteField {
// css样式
style?: string;
// 名称
value?: string;
value?: any;
// 语言
lang?: string;
// 是否加密
encrypted?: boolean;
// 模板
template?: string;
// 排序(数字越小越靠前)

View File

@@ -8,7 +8,9 @@
:placeholder="placeholder"
/>
<a-button @click="openEdit">
<template #icon><BulbOutlined class="ele-text-warning" /></template>
<template #icon>
<BulbOutlined class="ele-text-warning"/>
</template>
</a-button>
</a-input-group>
<!-- 选择弹窗 -->
@@ -23,40 +25,40 @@
</template>
<script lang="ts" setup>
import { BulbOutlined } from '@ant-design/icons-vue';
import { ref } from 'vue';
import SelectData from './components/select-data.vue';
import { Company } from '@/api/system/company/model';
import {BulbOutlined} from '@ant-design/icons-vue';
import {ref} from 'vue';
import SelectData from './components/select-data.vue';
import {CmsWebsiteField} from "@/api/cms/cmsWebsiteField/model";
const props = withDefaults(
defineProps<{
value?: any;
customerType?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择数据'
}
);
defineProps<{
value?: any;
customerType?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择数据'
}
);
const emit = defineEmits<{
(e: 'done', Customer): void;
(e: 'clear'): void;
}>();
const emit = defineEmits<{
(e: 'done', data: CmsWebsiteField): void;
(e: 'clear'): void;
}>();
// 是否显示编辑弹窗
const showEdit = ref(false);
// 当前编辑数据
const current = ref<Company | null>(null);
const content = ref<any>(props.value)
// 是否显示编辑弹窗
const showEdit = ref(false);
// 当前编辑数据
const current = ref<CmsWebsiteField | null>(null);
const content = ref<any>(props.value)
/* 打开编辑弹窗 */
const openEdit = (row?: Company) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开编辑弹窗 */
const openEdit = (row?: CmsWebsiteField) => {
current.value = row ?? null;
showEdit.value = true;
};
const onChange = () => {
emit('done', content.value);
};
const onChange = (item: CmsWebsiteField) => {
emit('done', item);
};
</script>

View File

@@ -13,18 +13,18 @@
>
移动
</a-button>
<a-tree-select
allow-clear
:tree-data="navigationList"
tree-default-expand-all
style="width: 280px"
:listHeight="700"
placeholder="请选择栏目"
:value="where.categoryId || undefined"
:dropdown-style="{ overflow: 'auto' }"
@update:value="(value?: number) => (where.categoryId = value)"
@change="onCategoryId"
/>
<!-- <a-tree-select-->
<!-- allow-clear-->
<!-- :tree-data="navigationList"-->
<!-- tree-default-expand-all-->
<!-- style="width: 280px"-->
<!-- :listHeight="700"-->
<!-- placeholder="请选择栏目"-->
<!-- :value="where.categoryId || undefined"-->
<!-- :dropdown-style="{ overflow: 'auto' }"-->
<!-- @update:value="(value?: number) => (where.categoryId = value)"-->
<!-- @change="onCategoryId"-->
<!-- />-->
<a-input-search
allow-clear
placeholder="请输入关键词"

View File

@@ -23,7 +23,8 @@
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'icon'">
<a-image :src="record.icon" :width="50"/>
<a-image :src="record.icon" :width="50"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"/>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
@@ -124,17 +125,6 @@ const columns = ref<ColumnItem[]>([
// hideInSetting: true,
// customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
// },
{
title: '链接名称',
dataIndex: 'name',
key: 'name'
},
{
title: '所属栏目',
dataIndex: 'categoryName',
align: 'center',
width: 180,
},
{
title: '图标',
dataIndex: 'icon',
@@ -142,6 +132,11 @@ const columns = ref<ColumnItem[]>([
align: 'center',
width: 120,
},
{
title: '链接名称',
dataIndex: 'name',
key: 'name'
},
{
title: '链接地址',
dataIndex: 'url',
@@ -151,6 +146,13 @@ const columns = ref<ColumnItem[]>([
title: '备注',
dataIndex: 'comments'
},
{
title: '所属栏目',
dataIndex: 'categoryName',
align: 'center',
width: 180,
hideInTable: true
},
{
title: '状态',
dataIndex: 'status',

View File

@@ -56,6 +56,13 @@
v-model:value="form.websiteCode"
/>
</a-form-item>
<!-- <a-form-item label="AppSecret" name="websiteSecret" v-if="form.type == 20">-->
<!-- <a-input-password-->
<!-- allow-clear-->
<!-- placeholder="请输入AppSecret"-->
<!-- v-model:value="form.websiteSecret"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="小程序描述" name="comments">
<a-textarea
:rows="4"
@@ -64,9 +71,6 @@
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="账号类型" name="type">
{{ form.websiteType }}
</a-form-item>
<a-form-item label="小程序码" name="avatar">
<SelectFile
:placeholder="`请选择图片`"
@@ -140,6 +144,7 @@ import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FileRecord} from '@/api/system/file/model';
import {updateCmsDomain} from '@/api/cms/cmsDomain';
import {updateTenant} from "@/api/system/tenant";
import {decrypt, encrypt} from "@/utils/common";
// 是否是修改
const isUpdate = ref(false);
@@ -177,6 +182,7 @@ const form = reactive<CmsWebsite>({
websiteLogo: undefined,
websiteName: undefined,
websiteCode: undefined,
websiteSecret: undefined,
type: 20,
files: undefined,
keywords: '',
@@ -243,6 +249,14 @@ const rules = reactive({
trigger: 'blur'
}
],
websiteSecret: [
{
required: true,
type: 'string',
message: '请填写小程序密钥',
trigger: 'blur'
}
],
adminUrl: [
{
required: true,
@@ -326,6 +340,7 @@ const save = () => {
...form,
type: 20,
adminUrl: `mp.websoft.top`,
websiteSecret: form.websiteSecret ? encrypt(form.websiteSecret) : undefined,
files: JSON.stringify(files.value),
};
saveOrUpdate(formData)
@@ -387,6 +402,9 @@ watch(
if (props.data.websiteCode) {
oldDomain.value = props.data.websiteCode;
}
if(props.data.websiteSecret){
form.websiteSecret = decrypt(props.data.websiteSecret);
}
isUpdate.value = true;
} else {
isUpdate.value = false;

View File

@@ -25,6 +25,7 @@
<div class="font-medium">{{ record.websiteName }}</div>
</template>
<template v-if="column.key === 'websiteCode'">
<div class="text-gray-400">{{ record.tenantId }}</div>
<div class="text-gray-400">
{{ record.websiteCode }}
</div>
@@ -177,9 +178,9 @@ const datasource: DatasourceFunction = ({
// 表格列配置
const columns = ref<ColumnItem[]>([
// {
// title: 'ID',
// dataIndex: 'websiteId',
// key: 'websiteId',
// title: 'AppID',
// dataIndex: 'tenantId',
// key: 'tenantId',
// width: 90
// },
{
@@ -198,16 +199,15 @@ const columns = ref<ColumnItem[]>([
{
title: '小程序信息',
dataIndex: 'websiteCode',
key: 'websiteCode',
align: 'center'
},
{
title: '小程序描述',
dataIndex: 'comments',
key: 'comments',
align: 'center'
key: 'websiteCode'
},
// {
// title: '小程序描述',
// dataIndex: 'comments',
// key: 'comments',
// align: 'center'
// },
// {
// title: '当前版本',
// dataIndex: 'version',
// key: 'version',

View File

@@ -13,15 +13,15 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 4 }, sm: { span: 4 }, xs: { span: 24 } }"
:label-col="{ md: { span: 3 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-form-item label="字段名称" name="name">
<a-form-item label="名称" name="name">
<div class="w-full flex justify-between">
<a-input
allow-clear
:maxlength="100"
placeholder="siteName"
placeholder="SiteName"
class="px-5 mr-2"
v-model:value="form.name"
/>
@@ -33,13 +33,14 @@
/>
</div>
</a-form-item>
<a-form-item label="字段类型" name="type">
<a-form-item label="类型" name="type">
<a-space direction="vertical" class="w-full">
<div class="p-1">
<a-radio-group v-model:value="form.type">
<a-radio :value="0">文本</a-radio>
<a-radio :value="1">图片</a-radio>
<a-radio :value="2">视频</a-radio>
<a-radio :value="3">开关</a-radio>
</a-radio-group>
</div>
<template v-if="form.type == 1">
@@ -55,7 +56,7 @@
请选择或上传图片
</div>
</template>
<template v-if="form.type == 2">
<template v-else-if="form.type == 2">
<SelectFile
:placeholder="`请选择视频`"
:limit="1"
@@ -68,33 +69,32 @@
请选择或上传视频
</div>
</template>
<template v-else-if="form.type === 3">
<a-switch v-model:checked="form.value"/>
</template>
<template v-else>
<a-input placeholder="字段内容" v-model:value="form.value" />
</template>
</a-space>
</a-form-item>
<a-form-item label="字段内容" name="value">
<a-textarea
:rows="4"
:maxlength="2000"
placeholder="XXXX有限公司"
v-model:value="form.value"
/>
</a-form-item>
<a-form-item label="字段描述" name="comments">
<a-form-item label="描述" name="comments">
<a-textarea
:rows="2"
:maxlength="200"
placeholder="网站名称"
:maxlength="2000"
placeholder="描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="style" name="style">
<a-form-item label="预留" name="style">
<a-input
allow-clear
placeholder="style"
placeholder="预留字段"
style="width: 325px"
v-model:value="form.style"
/>
<div class="pt-2">
<a class="text-sm text-gray-500" href="https://www.tailwindcss.cn/docs/installation" target="_blank">Tailwind Css使用教程</a>
</div>
</a-form-item>
<a-form-item label="加密" name="encrypted">
<a-switch v-model:checked="form.encrypted"></a-switch>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input-number
@@ -109,182 +109,207 @@
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { FormInstance } from 'ant-design-vue/es/form';
import useFormData from '@/utils/use-form-data';
import {
addCmsWebsiteField,
updateCmsWebsiteField
} from '@/api/cms/cmsWebsiteField';
import { message } from 'ant-design-vue/es';
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FileRecord } from '@/api/system/file/model';
import { uuid } from 'ele-admin-pro';
import { useI18n } from 'vue-i18n';
import { CmsWebsiteField } from '@/api/cms/cmsWebsiteField/model';
import {ref, reactive, watch} from 'vue';
import {FormInstance} from 'ant-design-vue/es/form';
import useFormData from '@/utils/use-form-data';
import {
addCmsWebsiteField,
updateCmsWebsiteField
} from '@/api/cms/cmsWebsiteField';
import {message} from 'ant-design-vue/es';
import {removeSiteInfoCache} from '@/api/cms/cmsWebsite';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FileRecord} from '@/api/system/file/model';
import {uuid} from 'ele-admin-pro';
import {useI18n} from 'vue-i18n';
import {CmsWebsiteField} from '@/api/cms/cmsWebsiteField/model';
import {decrypt, encrypt} from "@/utils/common";
// 是否是修改
const isUpdate = ref(false);
// 是否是修改
const isUpdate = ref(false);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
websiteId: number | null | undefined;
// 修改回显的数据
data?: CmsWebsiteField | null;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
websiteId: number | null | undefined;
// 修改回显的数据
data?: CmsWebsiteField | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
const images = ref<ItemType[]>([]);
const formRef = ref<FormInstance | null>(null);// 国际化
const { locale } = useI18n();
// 提交状态
const loading = ref(false);
const images = ref<ItemType[]>([]);
const formRef = ref<FormInstance | null>(null);// 国际化
const {locale} = useI18n();
const { form, resetFields, assignFields } = useFormData<CmsWebsiteField>({
id: undefined,
type: 0,
template: undefined,
name: undefined,
value: undefined,
modifyRange: undefined,
defaultValue: undefined,
comments: '',
style: '',
lang: '',
sortNumber: 100
const {form, resetFields, assignFields} = useFormData<CmsWebsiteField>({
id: undefined,
type: 0,
template: undefined,
name: undefined,
value: undefined,
modifyRange: undefined,
defaultValue: undefined,
comments: '',
style: '',
lang: '',
encrypted: false,
sortNumber: 100
});
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请输入名称'
}
],
comments: [
{
required: true,
type: 'string',
message: '请输入字段描述'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择字段类型'
}
],
value: [
{
required: true,
type: 'string',
message: '请填写字段值'
}
]
// comments: [
// {
// required: true,
// type: 'string',
// message: '请输入字段'
// }
// ],
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.value = data.downloadUrl;
form.type = 1;
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请输入字段名称(英语)'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择字段类型'
}
],
value: [
{
required: true,
type: 'string',
message: '请填写字段值'
}
]
// comments: [
// {
// required: true,
// type: 'string',
// message: '请输入字段'
// }
// ],
});
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.type = 0;
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const chooseImage = (data: FileRecord) => {
const chooseData = (data: CmsWebsiteField) => {
form.name = data.name;
form.value = data.defaultValue;
form.comments = data.comments;
if (data.type == 1) {
images.value.push({
uid: data.id,
url: data.path,
uid: `${data.id}`,
url: data.defaultValue,
status: 'done'
});
form.value = data.downloadUrl;
form.type = 1;
};
}
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.type = 0;
};
const chooseData = (data: CmsWebsiteField) => {
form.name = data.name;
form.value = data.defaultValue;
if (data.type == 1) {
images.value.push({
uid: `${data.id}`,
url: data.defaultValue,
status: 'done'
});
}
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const data = {
...form,
// name: form.name?.toUpperCase(),
websiteId: props.websiteId,
lang: locale.value
};
const saveOrUpdate = isUpdate.value
? updateCmsWebsiteField
: addCmsWebsiteField;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
// 清除缓存
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId'));
if (form.name == 'i18n') {
localStorage.setItem('i18n','1');
window.location.reload();
}
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignFields(props.data);
form.comments = props.data.comments;
if (form.type == 1) {
images.value.push({
uid: uuid(),
url: props.data.value,
status: 'done'
});
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
// 3 开关
if (form.type == 3) {
form.value = form.value ? '1' : '0';
}
const data = {
...form,
// name: form.name?.toUpperCase(),
websiteId: props.websiteId,
lang: locale.value
};
// 加密处理
if (data.encrypted) {
data.value = encrypt(data.value);
}
const saveOrUpdate = isUpdate.value
? updateCmsWebsiteField
: addCmsWebsiteField;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
// 清除缓存
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId'));
if (form.name == 'i18n') {
localStorage.setItem('i18n', '1');
window.location.reload();
}
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignFields(props.data);
form.comments = props.data.comments;
if (form.type == 1) {
images.value.push({
uid: uuid(),
url: props.data.value,
status: 'done'
});
}
if (form.type == 3) {
form.value = props.data.value == '1';
}
if (form.encrypted) {
form.value = decrypt(props.data.value);
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
);
}
);
</script>

View File

@@ -8,8 +8,8 @@
v-model:value="where.keywords"
@search="reload"
/>
<a-button type="text" v-if="hasRole('superAdmin')" @click="handleExport">导出xls</a-button>
<a-button type="text" v-if="hasRole('superAdmin')" @click="openImport">导入xls</a-button>
<a-button type="dashed" :disabled="!hasRole('superAdmin')" @click="handleExport">导出xls</a-button>
<a-button type="dashed" :disabled="!hasRole('superAdmin')" @click="openImport">导入xls</a-button>
</a-space>
<!-- 导入弹窗 -->
<import v-model:visible="showImport" @done="reload"/>

View File

@@ -22,30 +22,45 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="ele-text-heading" @mouseover="onCopyIcon(record.name)" @mouseleave="hideCopyIcon">
{{ record.name }}
<CopyOutlined class="px-2" v-if="currentName == record.name"
@click="copyText(`config.${record.name}`)"/>
<span>{{ record.name }}</span>
<CopyOutlined class="px-2" style="color: #cccccc" @click="copyText(`${record.value}`)"/>
</div>
<div class="text-gray-300">{{ record.comments }}</div>
<template v-if="record.value">
<a-space>
<div class="text-gray-400" v-if="record.encrypted">{{ decrypt(record.value) }}</div>
<div class="text-gray-400" v-else>{{ record.value }}</div>
</a-space>
</template>
</template>
<template v-if="column.key === 'value'">
<a-image
v-if="record.type === 1"
:src="record.value"
:width="120"
/>
<div v-else v-html="record.value"></div>
<a-space>
<a-image
v-if="record.type === 1"
:src="record.value"
:width="120"
/>
<video
v-else-if="record.type === 2"
:src="record.value"
style="background-color: #000000"
:width="120"
:height="120"
/>
<a-switch v-else-if="record.type == 3" :checked="record.value == '1'" @change="updateValue(record)"/>
<span v-else-if="record.name === 'AppSecret'">
{{ decrypt(record.value) }}
</span>
<div v-else v-html="record.value"></div>
</a-space>
</template>
<template v-if="column.key === 'encrypted'">
<a-tag v-if="record.encrypted">加密</a-tag>
</template>
<template v-if="column.key === 'comments'">
<a-popover>
<template #content>
{{ record.comments }}
</template>
<ExclamationCircleOutlined/>
</a-popover>
<span class="text-gray-400">{{ record.comments }}</span>
</template>
<template v-if="column.key === 'action'">
<a @click="copyText(`config.${record.name}`)">复制</a>
<a @click="copyText(`${record.name}`)">复制</a>
<a-divider type="vertical"/>
<a @click="openEdit(record)">编辑</a>
<template v-if="record.deleted == 0">
@@ -81,7 +96,7 @@ import {ref, watch} from 'vue';
import {useI18n} from 'vue-i18n';
import {message} from 'ant-design-vue';
import type {EleProTable} from 'ele-admin-pro';
import {ExclamationCircleOutlined, CopyOutlined} from '@ant-design/icons-vue';
import {CopyOutlined} from '@ant-design/icons-vue';
import type {DatasourceFunction} from 'ele-admin-pro/es/ele-pro-table/types';
import CmsWebsiteSearch from "@/views/cms/cmsWebsite/components/search.vue";
import Search from './components/search.vue';
@@ -93,9 +108,9 @@ import {
import {
listCmsWebsiteField,
removeCmsWebsiteField,
undeleteWebsiteField
undeleteWebsiteField, updateCmsWebsiteField
} from '@/api/cms/cmsWebsiteField';
import {copyText, getPageTitle} from '@/utils/common';
import {copyText, decrypt, getPageTitle} from '@/utils/common';
const props = defineProps<{
websiteId: any;
@@ -137,20 +152,24 @@ const columns = ref<any[]>([
key: 'name',
ellipsis: true
},
{
title: '值',
dataIndex: 'value',
key: 'value',
ellipsis: true
},
// {
// title: '值',
// dataIndex: 'value',
// key: 'value',
// ellipsis: true
// },
{
title: '描述',
dataIndex: 'comments',
key: 'comments',
width: 120,
align: 'center',
ellipsis: true
},
{
title: '加密',
dataIndex: 'encrypted',
key: 'encrypted',
width: 120
},
{
title: '排序',
dataIndex: 'sortNumber',
@@ -186,6 +205,23 @@ const reload = (where?: CmsWebsiteFieldParam) => {
tableRef?.value?.reload({where: where});
};
const updateValue = (row: CmsWebsiteField) => {
const hide = message.loading('请求中..', 0);
updateCmsWebsiteField({
...row,
value: row.value == '1' ? '0' : '1'
})
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 删除单个 */
const remove = (row: CmsWebsiteField) => {
const hide = message.loading('请求中..', 0);

View File

@@ -0,0 +1,172 @@
<!-- 搜索表单 -->
<template>
<div class="search-container">
<!-- 搜索表单 -->
<a-form
:model="searchForm"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="订单编号">
<a-input
v-model:value="searchForm.orderId"
placeholder="请输入订单编号"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="商品名称">
<a-input
v-model:value="searchForm.productName"
placeholder="请输入商品名称"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="订单状态">
<a-select
v-model:value="searchForm.isInvalid"
placeholder="全部"
allow-clear
style="width: 120px"
>
<a-select-option :value="0">有效</a-select-option>
<a-select-option :value="1">失效</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="结算状态">
<a-select
v-model:value="searchForm.isSettled"
placeholder="全部"
allow-clear
style="width: 120px"
>
<a-select-option :value="0">未结算</a-select-option>
<a-select-option :value="1">已结算</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit" class="ele-btn-icon">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="resetSearch">
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-space>
<a-button
type="primary"
:disabled="!selection?.length"
@click="batchSettle"
class="ele-btn-icon"
>
<template #icon>
<DollarOutlined />
</template>
批量结算
</a-button>
</a-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import {
SearchOutlined,
DollarOutlined,
ExportOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerOrderParam } from '@/api/shop/shopDealerOrder/model';
const props = withDefaults(
defineProps<{
// 选中的数据
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerOrderParam): void;
(e: 'batchSettle'): void;
(e: 'export'): void;
}>();
// 搜索表单
const searchForm = reactive<ShopDealerOrderParam>({
orderId: undefined,
orderNo: '',
productName: '',
isInvalid: undefined,
isSettled: undefined
});
// 搜索
const handleSearch = () => {
const searchParams = { ...searchForm };
// 清除空值
Object.keys(searchParams).forEach(key => {
if (searchParams[key] === '' || searchParams[key] === undefined) {
delete searchParams[key];
}
});
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
Object.keys(searchForm).forEach(key => {
searchForm[key] = key === 'orderId' ? undefined : '';
});
emit('search', {});
};
// 批量结算
const batchSettle = () => {
emit('batchSettle');
};
// 导出数据
const exportData = () => {
emit('export');
};
</script>
<style lang="less" scoped>
.search-container {
background: #fff;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
.search-form {
margin-bottom: 16px;
:deep(.ant-form-item) {
margin-bottom: 8px;
}
}
.action-buttons {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
}
</style>

View File

@@ -0,0 +1,458 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销订单记录' : '添加分销订单记录'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<!-- 订单基本信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">订单基本信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="买家用户ID" name="userId">
<a-input-number
:min="1"
placeholder="请输入买家用户ID"
v-model:value="form.userId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="订单ID" name="orderId">
<a-input-number
:min="1"
placeholder="请输入订单ID"
v-model:value="form.orderId"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="订单总金额" name="orderPrice">
<a-input-number
:min="0"
:precision="2"
placeholder="请输入订单总金额(不含运费)"
v-model:value="form.orderPrice"
style="width: 300px"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
<!-- 分销商信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">分销商信息</span>
</a-divider>
<!-- 一级分销商 -->
<div class="dealer-section">
<h4 class="dealer-title">
<a-tag color="red">一级分销商</a-tag>
</h4>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户ID" name="firstUserId">
<a-input-number
:min="1"
placeholder="请输入一级分销商用户ID"
v-model:value="form.firstUserId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="分销佣金" name="firstMoney">
<a-input-number
:min="0"
:precision="2"
placeholder="请输入一级分销佣金"
v-model:value="form.firstMoney"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 二级分销商 -->
<div class="dealer-section">
<h4 class="dealer-title">
<a-tag color="orange">二级分销商</a-tag>
</h4>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户ID" name="secondUserId">
<a-input-number
:min="1"
placeholder="请输入二级分销商用户ID"
v-model:value="form.secondUserId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="分销佣金" name="secondMoney">
<a-input-number
:min="0"
:precision="2"
placeholder="请输入二级分销佣金"
v-model:value="form.secondMoney"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 三级分销商 -->
<div class="dealer-section">
<h4 class="dealer-title">
<a-tag color="gold">三级分销商</a-tag>
</h4>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户ID" name="thirdUserId">
<a-input-number
:min="1"
placeholder="请输入三级分销商用户ID"
v-model:value="form.thirdUserId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="分销佣金" name="thirdMoney">
<a-input-number
:min="0"
:precision="2"
placeholder="请输入三级分销佣金"
v-model:value="form.thirdMoney"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 状态信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">状态信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="订单状态" name="isInvalid">
<a-radio-group v-model:value="form.isInvalid">
<a-radio :value="0">有效</a-radio>
<a-radio :value="1">失效</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="结算状态" name="isSettled">
<a-radio-group v-model:value="form.isSettled">
<a-radio :value="0">未结算</a-radio>
<a-radio :value="1">已结算</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="结算时间" name="settleTime" v-if="form.isSettled === 1">
<a-date-picker
v-model:value="form.settleTime"
show-time
placeholder="请选择结算时间"
style="width: 300px"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { assignObject, uuid } from 'ele-admin-pro';
import { addShopDealerOrder, updateShopDealerOrder } from '@/api/shop/shopDealerOrder';
import { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerOrder | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 表单数据
const form = reactive<ShopDealerOrder>({
id: undefined,
userId: undefined,
orderId: undefined,
orderPrice: undefined,
firstUserId: undefined,
secondUserId: undefined,
thirdUserId: undefined,
firstMoney: undefined,
secondMoney: undefined,
thirdMoney: undefined,
isInvalid: 0,
isSettled: 0,
settleTime: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
userId: [
{
required: true,
message: '请输入买家用户ID',
trigger: 'blur'
}
],
orderId: [
{
required: true,
message: '请输入订单ID',
trigger: 'blur'
}
],
orderPrice: [
{
required: true,
message: '请输入订单总金额',
trigger: 'blur'
}
],
firstUserId: [
{
validator: (rule: any, value: any) => {
if (form.firstMoney && !value) {
return Promise.reject('设置了一级佣金必须填写一级分销商用户ID');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
firstMoney: [
{
validator: (rule: any, value: any) => {
if (form.firstUserId && !value) {
return Promise.reject('设置了一级分销商必须填写一级佣金');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
secondUserId: [
{
validator: (rule: any, value: any) => {
if (form.secondMoney && !value) {
return Promise.reject('设置了二级佣金必须填写二级分销商用户ID');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
secondMoney: [
{
validator: (rule: any, value: any) => {
if (form.secondUserId && !value) {
return Promise.reject('设置了二级分销商必须填写二级佣金');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
thirdUserId: [
{
validator: (rule: any, value: any) => {
if (form.thirdMoney && !value) {
return Promise.reject('设置了三级佣金必须填写三级分销商用户ID');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
thirdMoney: [
{
validator: (rule: any, value: any) => {
if (form.thirdUserId && !value) {
return Promise.reject('设置了三级分销商必须填写三级佣金');
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateShopDealerOrder : addShopDealerOrder;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
// 处理时间字段
if (props.data.settleTime) {
form.settleTime = dayjs(props.data.settleTime);
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
id: undefined,
userId: undefined,
orderId: undefined,
orderPrice: undefined,
firstUserId: undefined,
secondUserId: undefined,
thirdUserId: undefined,
firstMoney: undefined,
secondMoney: undefined,
thirdMoney: undefined,
isInvalid: 0,
isSettled: 0,
settleTime: undefined
});
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
.dealer-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
border-left: 3px solid #1890ff;
.dealer-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #333;
.ant-tag {
margin-right: 8px;
}
}
}
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
margin: 24px 0 16px 0;
.ant-divider-inner-text {
padding: 0 16px 0 0;
}
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-input-number) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,506 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopDealerOrderId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@batchSettle="batchSettle"
@export="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'orderInfo'">
<div class="order-info">
<div class="order-id">订单号: {{ record.orderId || '-' }}</div>
<div class="order-price">金额: ¥{{ parseFloat(record.orderPrice || '0').toFixed(2) }}</div>
</div>
</template>
<template v-if="column.key === 'dealerInfo'">
<div class="dealer-info">
<div v-if="record.firstUserId" class="dealer-level">
<a-tag color="red">一级</a-tag>
用户{{ record.firstUserId }} - ¥{{ parseFloat(record.firstMoney || '0').toFixed(2) }}
</div>
<div v-if="record.secondUserId" class="dealer-level">
<a-tag color="orange">二级</a-tag>
用户{{ record.secondUserId }} - ¥{{ parseFloat(record.secondMoney || '0').toFixed(2) }}
</div>
<div v-if="record.thirdUserId" class="dealer-level">
<a-tag color="gold">三级</a-tag>
用户{{ record.thirdUserId }} - ¥{{ parseFloat(record.thirdMoney || '0').toFixed(2) }}
</div>
</div>
</template>
<template v-if="column.key === 'isInvalid'">
<a-tag v-if="record.isInvalid === 0" color="success">有效</a-tag>
<a-tag v-if="record.isInvalid === 1" color="error">失效</a-tag>
</template>
<template v-if="column.key === 'isSettled'">
<a-tag v-if="record.isSettled === 0" color="processing">未结算</a-tag>
<a-tag v-if="record.isSettled === 1" color="success">已结算</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="viewDetail(record)" class="ele-text-info">
<EyeOutlined/>
详情
</a>
<template v-if="record.isSettled === 0 && record.isInvalid === 0">
<a-divider type="vertical"/>
<a @click="settleOrder(record)" class="ele-text-success">
<DollarOutlined/>
结算
</a>
</template>
<template v-if="record.isInvalid === 0">
<a-divider type="vertical"/>
<a-popconfirm
title="确定要标记此订单为失效吗?"
@confirm="invalidateOrder(record)"
placement="topRight"
>
<a class="ele-text-warning">
<CloseOutlined/>
失效
</a>
</a-popconfirm>
</template>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopDealerOrderEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {
ExclamationCircleOutlined,
EyeOutlined,
DollarOutlined,
CloseOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import ShopDealerOrderEdit from './components/shopDealerOrderEdit.vue';
import {pageShopDealerOrder, removeShopDealerOrder, removeBatchShopDealerOrder} from '@/api/shop/shopDealerOrder';
import type {ShopDealerOrder, ShopDealerOrderParam} from '@/api/shop/shopDealerOrder/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerOrder[]>([]);
// 当前编辑数据
const current = ref<ShopDealerOrder | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopDealerOrder({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '商品信息',
key: 'productInfo',
align: 'left',
width: 200,
customRender: ({record}) => {
return `商品ID: ${record.productId || '-'}`;
}
},
{
title: '单价/数量',
key: 'priceInfo',
align: 'center',
width: 120,
customRender: ({record}) => {
return `¥${parseFloat(record.unitPrice || '0').toFixed(2)} × ${record.quantity || 1}`;
}
},
{
title: '订单信息',
key: 'orderInfo',
align: 'left',
width: 180
},
{
title: '买家',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 100,
customRender: ({text}) => `用户${text || '-'}`
},
{
title: '分销商信息',
key: 'dealerInfo',
align: 'left',
width: 300
},
{
title: '订单状态',
dataIndex: 'isInvalid',
key: 'isInvalid',
align: 'center',
width: 100,
filters: [
{text: '有效', value: 0},
{text: '失效', value: 1}
]
},
{
title: '结算状态',
dataIndex: 'isSettled',
key: 'isSettled',
align: 'center',
width: 100,
filters: [
{text: '未结算', value: 0},
{text: '已结算', value: 1}
]
},
{
title: '结算时间',
dataIndex: 'settleTime',
key: 'settleTime',
align: 'center',
width: 120,
customRender: ({text}) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 180,
sorter: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm')
},
{
title: '操作',
key: 'action',
width: 240,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopDealerOrderParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 查看订单详情 */
const viewDetail = (row: ShopDealerOrder) => {
Modal.info({
title: '分销订单详情',
width: 800,
content: createVNode('div', {style: 'max-height: 500px; overflow-y: auto;'}, [
createVNode('div', {class: 'detail-section'}, [
createVNode('h4', null, '订单基本信息'),
createVNode('p', null, `订单ID: ${row.orderId || '-'}`),
createVNode('p', null, `买家用户ID: ${row.userId || '-'}`),
createVNode('p', null, `订单金额: ¥${parseFloat(row.orderPrice || '0').toFixed(2)}`),
createVNode('p', null, `创建时间: ${row.createTime ? toDateString(row.createTime, 'yyyy-MM-dd HH:mm:ss') : '-'}`),
]),
createVNode('div', {class: 'detail-section', style: 'margin-top: 16px;'}, [
createVNode('h4', null, '分销商信息'),
...(row.firstUserId ? [
createVNode('div', {style: 'margin: 8px 0; padding: 8px; background: #fff2f0; border-left: 3px solid #ff4d4f;'}, [
createVNode('strong', null, '一级分销商'),
createVNode('p', null, `用户ID: ${row.firstUserId}`),
createVNode('p', null, `佣金: ¥${parseFloat(row.firstMoney || '0').toFixed(2)}`)
])
] : []),
...(row.secondUserId ? [
createVNode('div', {style: 'margin: 8px 0; padding: 8px; background: #fff7e6; border-left: 3px solid #fa8c16;'}, [
createVNode('strong', null, '二级分销商'),
createVNode('p', null, `用户ID: ${row.secondUserId}`),
createVNode('p', null, `佣金: ¥${parseFloat(row.secondMoney || '0').toFixed(2)}`)
])
] : []),
...(row.thirdUserId ? [
createVNode('div', {style: 'margin: 8px 0; padding: 8px; background: #fffbe6; border-left: 3px solid #fadb14;'}, [
createVNode('strong', null, '三级分销商'),
createVNode('p', null, `用户ID: ${row.thirdUserId}`),
createVNode('p', null, `佣金: ¥${parseFloat(row.thirdMoney || '0').toFixed(2)}`)
])
] : [])
]),
createVNode('div', {class: 'detail-section', style: 'margin-top: 16px;'}, [
createVNode('h4', null, '状态信息'),
createVNode('p', null, [
'订单状态: ',
createVNode('span', {
style: `color: ${row.isInvalid === 0 ? '#52c41a' : '#ff4d4f'}; font-weight: bold;`
}, row.isInvalid === 0 ? '有效' : '失效')
]),
createVNode('p', null, [
'结算状态: ',
createVNode('span', {
style: `color: ${row.isSettled === 1 ? '#52c41a' : '#1890ff'}; font-weight: bold;`
}, row.isSettled === 1 ? '已结算' : '未结算')
]),
createVNode('p', null, `结算时间: ${row.settleTime ? toDateString(new Date(row.settleTime), 'yyyy-MM-dd HH:mm:ss') : '-'}`),
])
]),
okText: '关闭'
});
};
/* 结算单个订单 */
const settleOrder = (row: ShopDealerOrder) => {
const totalCommission = (parseFloat(row.firstMoney || '0') +
parseFloat(row.secondMoney || '0') +
parseFloat(row.thirdMoney || '0')).toFixed(2);
Modal.confirm({
title: '确认结算',
content: `确定要结算此订单的佣金吗?总佣金金额:¥${totalCommission}`,
icon: createVNode(DollarOutlined),
okText: '确认结算',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在结算...', 0);
// 这里调用结算API
setTimeout(() => {
hide();
message.success('结算成功');
reload();
}, 1000);
}
});
};
/* 标记订单失效 */
const invalidateOrder = (row: ShopDealerOrder) => {
const hide = message.loading('正在处理...', 0);
// 这里调用失效API
setTimeout(() => {
hide();
message.success('订单已标记为失效');
reload();
}, 1000);
};
/* 批量结算 */
const batchSettle = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const validOrders = selection.value.filter(order =>
order.isSettled === 0 && order.isInvalid === 0
);
if (!validOrders.length) {
message.error('所选订单中没有可结算的订单');
return;
}
const totalCommission = validOrders.reduce((sum, order) => {
return sum + parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0');
}, 0).toFixed(2);
Modal.confirm({
title: '批量结算确认',
content: `确定要结算选中的 ${validOrders.length} 个订单吗?总佣金金额:¥${totalCommission}`,
icon: createVNode(ExclamationCircleOutlined),
okText: '确认结算',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在批量结算...', 0);
// 这里调用批量结算API
setTimeout(() => {
hide();
message.success(`成功结算 ${validOrders.length} 个订单`);
reload();
}, 1500);
}
});
};
/* 导出数据 */
const exportData = () => {
const hide = message.loading('正在导出数据...', 0);
// 这里调用导出API
setTimeout(() => {
hide();
message.success('数据导出成功');
}, 2000);
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerOrder) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerOrder) => {
const hide = message.loading('请求中..', 0);
removeShopDealerOrder(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerOrder(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerOrder) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerOrder'
};
</script>
<style lang="less" scoped>
.order-info {
.order-id {
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.order-price {
color: #ff4d4f;
font-weight: 600;
}
}
.dealer-info {
.dealer-level {
margin-bottom: 6px;
font-size: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
:deep(.detail-section) {
h4 {
color: #1890ff;
margin-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 8px;
}
p {
margin: 4px 0;
line-height: 1.5;
}
}
:deep(.ant-table-tbody > tr > td) {
vertical-align: top;
}
:deep(.ant-tag) {
margin: 2px 4px 2px 0;
}
</style>

View File

@@ -17,11 +17,11 @@
</a-col>
<a-col :span="14">
<div class="system-info">
<h2 class="ele-text-heading">{{ siteStore.appName }}</h2>
<h2 class="ele-text-heading cursor-pointer" @click="$router.push('/website/index')">{{ siteStore.appName }}</h2>
<p class="ele-text-secondary">{{ siteStore.description }}</p>
<a-space>
<a-tag color="blue">{{ siteStore.version }}</a-tag>
<a-tag color="green">{{ siteStore.statusText }}</a-tag>
<a-tag color="blue">{{ siteStore.version }}</a-tag>
<a-popover title="小程序码">
<template #content>
<p><img :src="siteStore.mpQrCode" alt="小程序码" width="300" height="300"></p>
@@ -138,11 +138,11 @@
<!-- 快捷操作 -->
<a-col :span="12">
<a-card title="快捷操作" :bordered="false">
<a-card title="快捷操作" :bordered="false" style="min-height: 353px">
<a-space direction="vertical" style="width: 100%">
<a-button type="primary" block @click="$router.push('/website/index')">
<ShopOutlined/>
站点管理
<a-button type="primary" block @click="$router.push('/website/field')" :loading="loading">
<UngroupOutlined/>
参数配置
</a-button>
<a-button block @click="$router.push('/shopOrder')">
<CalendarOutlined/>
@@ -152,22 +152,26 @@
<UserOutlined/>
用户管理
</a-button>
<a-button block @click="$router.push('/website/index')">
<ShopOutlined/>
站点管理
</a-button>
<!-- <a-button block @click="refreshStatistics" :loading="loading">-->
<!-- <ReloadOutlined/>-->
<!-- 刷新统计-->
<!-- </a-button>-->
<a-button block @click="$router.push('/system/login-record')">
<FileTextOutlined/>
登录日志
</a-button>
<a-button block @click="refreshStatistics" :loading="loading">
<ReloadOutlined/>
刷新统计
</a-button>
<a-button block @click="clearSiteInfoCache">
<ClearOutlined/>
清除缓存
</a-button>
<a-button block @click="$router.push('/system/setting')">
<SettingOutlined/>
系统设置
</a-button>
<!-- <a-button block @click="$router.push('/system/setting')">-->
<!-- <SettingOutlined/>-->
<!-- 系统设置-->
<!-- </a-button>-->
</a-space>
</a-card>
</a-col>
@@ -187,6 +191,7 @@ import {
AccountBookOutlined,
FileTextOutlined,
ClearOutlined,
UngroupOutlined,
MoneyCollectOutlined,
ReloadOutlined
} from '@ant-design/icons-vue';