Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	.env.development
This commit is contained in:
2025-11-20 22:29:42 +08:00
440 changed files with 49162 additions and 19543 deletions

View File

@@ -20,7 +20,7 @@
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="LOGO" name="avatar">
<a-form-item label="Logo" name="avatar">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
@@ -29,6 +29,9 @@
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="账号类型" name="type">
{{ form.type }}
</a-form-item>
<a-form-item label="小程序名称" name="websiteName">
<a-input
allow-clear
@@ -64,26 +67,14 @@
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="`请选择图片`"
:limit="1"
:data="websiteQrcode"
@done="chooseQrcode"
@del="onDeleteQrcode"
<a-form-item label="SEO关键词" name="keywords">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入SEO关键词"
v-model:value="form.keywords"
/>
</a-form-item>
<!-- <a-form-item label="SEO关键词" name="keywords">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
<!-- :maxlength="200"-->
<!-- placeholder="请输入SEO关键词"-->
<!-- v-model:value="form.keywords"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="全局样式" name="style">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
@@ -123,6 +114,7 @@
v-model:value="form.statusText"
/>
</a-form-item>
<!-- <a-divider style="margin-bottom: 24px" />-->
</a-form>
</ele-modal>
</template>
@@ -135,9 +127,10 @@ import {addCmsWebsite, updateCmsWebsite} from '@/api/cms/cmsWebsite';
import {CmsWebsite} from '@/api/cms/cmsWebsite/model';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {FormInstance} from 'ant-design-vue/es/form';
import {FormInstance, type Rule} from 'ant-design-vue/es/form';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FileRecord} from '@/api/system/file/model';
import {checkExistence} from '@/api/cms/cmsDomain';
import {updateCmsDomain} from '@/api/cms/cmsDomain';
import {updateTenant} from "@/api/system/tenant";
@@ -203,14 +196,14 @@ const updateVisible = (value: boolean) => {
//
const rules = reactive({
comments: [
{
required: true,
type: 'string',
message: '请填写小程序描述',
trigger: 'blur'
}
],
// comments: [
// {
// required: true,
// type: 'string',
// message: '',
// trigger: 'blur'
// }
// ],
keywords: [
{
required: true,
@@ -243,6 +236,31 @@ const rules = reactive({
trigger: 'blur'
}
],
// websiteCode: [
// {
// required: true,
// type: 'string',
// message: '使',
// validator: (_rule: Rule, value: string) => {
// return new Promise<void>((resolve, reject) => {
// if (!value) {
// return reject('');
// }
// checkExistence('domain', `${value}.wsdns.cn`)
// .then(() => {
// if (value === oldDomain.value) {
// return resolve();
// }
// reject('');
// })
// .catch(() => {
// resolve();
// });
// });
// },
// trigger: 'blur'
// }
// ],
adminUrl: [
{
required: true,
@@ -286,26 +304,28 @@ const chooseImage = (data: FileRecord) => {
form.websiteLogo = data.downloadUrl;
};
const chooseQrcode = (data: FileRecord) => {
websiteQrcode.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.websiteDarkLogo = data.downloadUrl;
}
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.websiteLogo = '';
};
const onDeleteQrcode = (index: number) => {
websiteQrcode.value.splice(index, 1);
form.websiteDarkLogo = '';
const chooseFile = (data: FileRecord) => {
form.websiteCode = data.url;
files.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
};
const onDeleteFile = (index: number) => {
files.value.splice(index, 1);
};
// const onWebsiteType = (text: string) => {
// form.websiteType = text;
// };
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
@@ -374,13 +394,6 @@ watch(
status: 'done'
});
}
if (props.data.websiteDarkLogo) {
websiteQrcode.value.push({
uid: uuid(),
url: props.data.websiteDarkLogo,
status: 'done'
});
}
if (props.data.files) {
files.value = JSON.parse(props.data.files);
}

View File

@@ -0,0 +1,295 @@
<template>
<a-page-header :show-back="false">
<a-row :gutter="16">
<!-- 应用基本信息卡片 -->
<a-col :span="24" style="margin-bottom: 16px">
<a-card title="概况" :bordered="false">
<a-row :gutter="16">
<a-col :span="6">
<a-image
:width="80"
:height="80"
:preview="false"
style="border-radius: 8px"
:src="siteStore.logo"
fallback="/logo.png"
/>
</a-col>
<a-col :span="14">
<div class="system-info">
<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="green">{{ siteStore.statusText }}</a-tag>
<a-tag color="blue" v-if="siteStore.version">{{ siteStore.version }}</a-tag>
<a-popover title="小程序码" v-if="siteStore.mpQrCode">
<template #content>
<p><img :src="siteStore.mpQrCode" alt="小程序码" width="300" height="300"></p>
</template>
<a-tag>
<QrcodeOutlined/>
</a-tag>
</a-popover>
</a-space>
</div>
</a-col>
<a-col :span="3">
<div class="flex justify-center items-center h-full w-full">
</div>
</a-col>
</a-row>
</a-card>
</a-col>
<!-- 统计数据卡片 -->
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="用户总数"
:value="userCount"
:value-style="{ color: '#3f8600' }"
:loading="loading"
>
<template #prefix>
<UserOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="订单总数"
:value="orderCount"
:value-style="{ color: '#1890ff' }"
:loading="loading"
>
<template #prefix>
<AccountBookOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="总营业额"
:value="totalSales"
:value-style="{ color: '#cf1322' }"
:loading="loading"
>
<template #prefix>
<MoneyCollectOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card :bordered="false" class="stat-card">
<a-statistic
title="系统运行天数"
:value="runDays"
suffix="天"
:value-style="{ color: '#722ed1' }"
:loading="loading"
>
<template #prefix>
<ClockCircleOutlined/>
</template>
</a-statistic>
</a-card>
</a-col>
<!-- 系统基本信息 -->
<a-col :span="12">
<a-card title="基本信息" :bordered="false">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="系统名称">
{{ systemInfo.name }}
</a-descriptions-item>
<a-descriptions-item label="版本号">
{{ systemInfo.version }}
</a-descriptions-item>
<a-descriptions-item label="部署环境">
{{ systemInfo.environment }}
</a-descriptions-item>
<a-descriptions-item label="数据库">
{{ systemInfo.database }}
</a-descriptions-item>
<a-descriptions-item label="服务器">
{{ systemInfo.server }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ siteInfo?.createTime }}
</a-descriptions-item>
<a-descriptions-item label="到期时间">
{{ siteInfo?.expirationTime }}
</a-descriptions-item>
<a-descriptions-item label="技术支持">
<span class="cursor-pointer" @click="openNew(`https://websoft.top`)">网宿软件</span>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<!-- 快捷操作 -->
<a-col :span="12">
<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/field')" :loading="loading">
<UngroupOutlined/>
参数配置
</a-button>
<a-button block @click="$router.push('/shopOrder')">
<CalendarOutlined/>
订单管理
</a-button>
<a-button block @click="$router.push('/system/user')">
<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="clearSiteInfoCache">
<ClearOutlined/>
清除缓存
</a-button>
<!-- <a-button block @click="$router.push('/system/setting')">-->
<!-- <SettingOutlined/>-->
<!-- 系统设置-->
<!-- </a-button>-->
</a-space>
</a-card>
</a-col>
</a-row>
</a-page-header>
</template>
<script lang="ts" setup>
import {ref, onMounted, onUnmounted, computed} from 'vue';
import {
UserOutlined,
CalendarOutlined,
QrcodeOutlined,
ShopOutlined,
ClockCircleOutlined,
AccountBookOutlined,
FileTextOutlined,
ClearOutlined,
UngroupOutlined,
MoneyCollectOutlined
} from '@ant-design/icons-vue';
import {message} from 'ant-design-vue/es';
import {openNew} from "@/utils/common";
import {useSiteStore} from '@/store/modules/site';
import {useStatisticsStore} from '@/store/modules/statistics';
import {storeToRefs} from 'pinia';
import {removeSiteInfoCache} from "@/api/cms/cmsWebsite";
// 使用状态管理
const siteStore = useSiteStore();
const statisticsStore = useStatisticsStore();
// 从 store 中获取响应式数据
const {siteInfo, loading: siteLoading} = storeToRefs(siteStore);
const {loading: statisticsLoading} = storeToRefs(statisticsStore);
// 系统信息
const systemInfo = ref({
name: '小程序开发',
description: '基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
version: '2.0.0',
status: '运行中',
logo: '/logo.png',
environment: '生产环境',
database: 'MySQL 8.0',
server: 'Linux CentOS 7.9',
expirationTime: '2024-01-01 09:00:00'
});
// 计算属性
const runDays = computed(() => siteStore.runDays);
const userCount = computed(() => statisticsStore.userCount);
const orderCount = computed(() => statisticsStore.orderCount);
const totalSales = computed(() => statisticsStore.totalSales);
// 加载状态
const loading = computed(() => siteLoading.value || statisticsLoading.value);
// 清除缓存
const clearSiteInfoCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
(msg) => {
if (msg) {
message.success(msg);
}
}
);
};
// 刷新统计数据
const refreshStatistics = async () => {
try {
await statisticsStore.forceRefresh();
message.success('统计数据刷新成功');
} catch (error) {
console.error('刷新统计数据失败:', error);
message.error('刷新统计数据失败');
}
};
onMounted(async () => {
// 加载网站信息和统计数据
try {
await Promise.all([
siteStore.fetchSiteInfo(),
statisticsStore.fetchStatistics()
]);
// 开始自动刷新统计数据每5分钟
statisticsStore.startAutoRefresh();
} catch (error) {
console.error('加载数据失败:', error);
}
});
onUnmounted(() => {
// 组件卸载时停止自动刷新
statisticsStore.stopAutoRefresh();
});
</script>
<style scoped>
.system-info h2 {
margin-bottom: 8px;
}
.stat-card {
text-align: center;
margin-bottom: 16px;
}
.stat-card :deep(.ant-statistic-title) {
font-size: 14px;
color: #666;
}
.stat-card :deep(.ant-statistic-content) {
font-size: 24px;
font-weight: 600;
}
</style>

View File

@@ -1,430 +0,0 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-spin :spinning="loading" class="page">
<a-card>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="LOGO" name="avatar">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="小程序名称" name="websiteName">
<a-input
allow-clear
placeholder="请输入小程序名称"
v-model:value="form.websiteName"
/>
</a-form-item>
<a-form-item label="网站域名" name="domain" v-if="form.type == 10">
<a-input
v-model:value="form.domain"
placeholder="huawei.com"
>
<template #addonBefore>
<a-select v-model:value="form.prefix" style="width: 90px">
<a-select-option value="http://">http://</a-select-option>
<a-select-option value="https://">https://</a-select-option>
</a-select>
</template>
</a-input>
</a-form-item>
<a-form-item label="AppId" name="websiteCode" v-if="form.type == 20">
<a-input
allow-clear
placeholder="请输入AppId"
v-model:value="form.websiteCode"
/>
</a-form-item>
<a-form-item label="小程序描述" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入小程序描述"
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="`请选择图片`"
:limit="1"
:data="websiteQrcode"
@done="chooseQrcode"
@del="onDeleteQrcode"
/>
</a-form-item>
<!-- <a-form-item label="SEO关键词" name="keywords">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
<!-- :maxlength="200"-->
<!-- placeholder="请输入SEO关键词"-->
<!-- v-model:value="form.keywords"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="全局样式" name="style">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
<!-- :maxlength="200"-->
<!-- placeholder="全局样式"-->
<!-- v-model:value="form.style"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="小程序类型" name="websiteType">-->
<!-- <a-select-->
<!-- :options="websiteType"-->
<!-- :value="form.websiteType"-->
<!-- placeholder="请选择主体类型"-->
<!-- @change="onCmsWebsiteType"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="当前版本" name="version">-->
<!-- <a-tag color="red" v-if="form.version === 10">标准版</a-tag>-->
<!-- <a-tag color="green" v-if="form.version === 20">专业版</a-tag>-->
<!-- <a-tag color="cyan" v-if="form.version === 30">永久授权</a-tag>-->
<!-- </a-form-item>-->
<a-form-item label="状态" name="running">
<a-radio-group
v-model:value="form.running"
:disabled="form.running == 4 || form.running == 5"
>
<a-radio :value="1">运行中</a-radio>
<a-radio :value="2">维护中</a-radio>
<a-radio :value="3">已关闭</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="form.running == 2" label="维护说明" name="statusText">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="状态说明"
v-model:value="form.statusText"
/>
</a-form-item>
</a-form>
</a-card>
</a-spin>
</a-page-header>
</template>
<script lang="ts" setup>
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject, uuid} from 'ele-admin-pro';
import {addCmsWebsite, updateCmsWebsite} from '@/api/cms/cmsWebsite';
import {CmsWebsite} from '@/api/cms/cmsWebsite/model';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {FormInstance} from 'ant-design-vue/es/form';
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 {getPageTitle, push} from "@/utils/common";
import router from "@/router";
import { useSiteStore } from '@/store/modules/site';
import useFormData from "@/utils/use-form-data";
import type {User} from "@/api/system/user/model";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const siteStore = useSiteStore();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
const siteInfo = ref<CmsWebsite>({})
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const websiteQrcode = ref<ItemType[]>([]);
const files = ref<ItemType[]>([]);
// 用户信息
const {form, assignFields} = useFormData<CmsWebsite>({
websiteId: undefined,
websiteLogo: undefined,
websiteName: undefined,
websiteCode: undefined,
type: 20,
files: undefined,
keywords: '',
prefix: '',
domain: '',
adminUrl: '',
style: '',
icpNo: undefined,
email: undefined,
version: undefined,
websiteType: '',
running: 1,
expirationTime: undefined,
sortNumber: undefined,
comments: undefined,
status: undefined,
statusText: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
comments: [
{
required: true,
type: 'string',
message: '请填写小程序描述',
trigger: 'blur'
}
],
keywords: [
{
required: true,
type: 'string',
message: '请填写SEO关键词',
trigger: 'blur'
}
],
running: [
{
required: true,
type: 'number',
message: '请选择小程序状态',
trigger: 'change'
}
],
domain: [
{
required: true,
type: 'string',
message: '请填写小程序域名',
trigger: 'blur'
}
],
websiteCode: [
{
required: true,
type: 'string',
message: '请填写小程序码',
trigger: 'blur'
}
],
adminUrl: [
{
required: true,
type: 'string',
message: '请填写小程序后台管理地址',
trigger: 'blur'
}
],
icpNo: [
{
required: true,
type: 'string',
message: '请填写ICP备案号',
trigger: 'blur'
}
],
appSecret: [
{
required: true,
type: 'string',
message: '请填写小程序秘钥',
trigger: 'blur'
}
],
websiteName: [
{
required: true,
type: 'string',
message: '请填写小程序信息名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.websiteLogo = data.downloadUrl;
};
const chooseQrcode = (data: FileRecord) => {
websiteQrcode.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.websiteDarkLogo = data.downloadUrl;
}
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.websiteLogo = '';
};
const onDeleteQrcode = (index: number) => {
websiteQrcode.value.splice(index, 1);
form.websiteDarkLogo = '';
};
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateCmsWebsite : addCmsWebsite;
if (!isUpdate.value) {
updateVisible(false);
message.loading('创建过程中请勿刷新页面!', 0)
}
const formData = {
...form,
type: 20,
adminUrl: `mp.websoft.top`,
files: JSON.stringify(files.value),
};
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
updateVisible(false);
updateCmsDomain({
websiteId: form.websiteId,
domain: `${localStorage.getItem('TenantId')}.shoplnk.cn`
});
updateTenant({
tenantName: `${form.websiteName}`
}).then(() => {
})
localStorage.setItem('Domain', `${form.websiteCode}.shoplnk.cn`);
localStorage.setItem('WebsiteId', `${form.websiteId}`);
localStorage.setItem('WebsiteName', `${form.websiteName}`);
message.destroy();
message.success(msg);
// window.location.reload();
emit('done');
})
.catch((e) => {
loading.value = false;
message.destroy();
message.error(e.message);
});
})
.catch(() => {
});
};
const reload = async () => {
try {
const data = await siteStore.fetchSiteInfo();
if (data) {
console.log(data);
assignFields({
...data
});
if (data.websiteLogo) {
images.value.push({
uid: uuid(),
url: data.websiteLogo,
status: 'done'
});
}
}
} catch (error) {
console.error('获取网站信息失败:', error);
}
}
watch(
() => router.currentRoute.value.query,
(query) => {
if (query) {
reload();
}
},
{immediate: true}
);
// watch(
// () => props.visible,
// (visible) => {
// if (visible) {
// images.value = [];
// files.value = [];
// websiteQrcode.value = [];
// if (props.data?.websiteId) {
// assignObject(form, props.data);
// if (props.data.websiteLogo) {
// images.value.push({
// uid: uuid(),
// url: props.data.websiteLogo,
// status: 'done'
// });
// }
// if (props.data.websiteDarkLogo) {
// websiteQrcode.value.push({
// uid: uuid(),
// url: props.data.websiteDarkLogo,
// status: 'done'
// });
// }
// if (props.data.files) {
// files.value = JSON.parse(props.data.files);
// }
// if (props.data.websiteCode) {
// oldDomain.value = props.data.websiteCode;
// }
// isUpdate.value = true;
// } else {
// isUpdate.value = false;
// }
// } else {
// resetFields();
// }
// },
// {immediate: true}
// );
</script>
<script lang="ts">
export default {
name: 'CmsWebsite'
};
</script>
<style lang="less" scoped></style>

View File

@@ -1,42 +0,0 @@
<template>
<a-card title="项目成员" style="margin-bottom: 20px">
<template #extra>
<a-space>
<a-button>编辑</a-button>
<a-button type="primary">添加</a-button>
</a-space>
</template>
<template v-for="(item,_) in list" class="bg-gray-50 rounded-lg w-80 p-4 flex justify-between items-center">
<a-space>
<a-avatar size="large" :src="item.avatar"/>
<div class="text-gray-400 flex flex-col">
<span>{{ item.nickname }}</span>
<span>{{ item.createTime }}</span>
</div>
</a-space>
</template>
</a-card>
</template>
<script lang="ts" setup>
import {ref, onMounted} from 'vue';
import {
listUsers
} from '@/api/system/user';
import {User} from "@/api/system/user/model";
const list = ref<User[]>([]);
const reload = async () => {
const data = await listUsers({
isAdmin: 1
});
if (data.length > 0) {
list.value = data;
}
}
onMounted(() => {
reload();
})
</script>

View File

@@ -0,0 +1,293 @@
<template>
<a-modal
:width="500"
:visible="visible"
:footer="null"
title="邀请注册"
@update:visible="updateVisible"
>
<div style="text-align: center">
<div style="margin-bottom: 20px">
<a-typography-title :level="4">邀请新成员注册</a-typography-title>
<a-typography-text type="secondary">
分享以下链接或二维码邀请新管理人员注册(非普通用户)
</a-typography-text>
</div>
<!-- 邀请链接 -->
<div style="margin-bottom: 20px">
<a-input
:value="invitationLink"
readonly
style="margin-bottom: 8px"
>
<template #addonAfter>
<a-button type="link" size="small" @click="copyLink">
复制链接
</a-button>
</template>
</a-input>
</div>
<!-- 二维码选择 -->
<div style="margin-bottom: 16px">
<a-radio-group v-model:value="qrCodeType" @change="onQRCodeTypeChange">
<a-radio-button value="web">网页二维码</a-radio-button>
<a-radio-button value="miniprogram">小程序码</a-radio-button>
</a-radio-group>
</div>
<!-- 二维码显示 -->
<div style="margin-bottom: 20px">
<div style="display: inline-block; padding: 10px; background: white; border-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1)">
<!-- 网页二维码 -->
<ele-qr-code-svg
v-if="qrCodeType === 'web'"
:value="invitationLink"
:size="200"
/>
<!-- 小程序码 -->
<div v-else-if="qrCodeType === 'miniprogram'" style="width: 200px; height: 200px; display: flex; align-items: center; justify-content: center;">
<img
v-if="miniProgramCodeUrl"
:src="miniProgramCodeUrl"
style="width: 180px; height: 180px; object-fit: contain;"
alt="小程序码"
@error="onMiniProgramCodeError"
@load="onMiniProgramCodeLoad"
/>
<a-spin v-else-if="loadingMiniCode" tip="正在生成小程序码..." />
<div v-else style="color: #999; text-align: center;">
<div>小程序码加载失败</div>
<a-button size="small" @click="loadMiniProgramCode">重新加载</a-button>
</div>
</div>
</div>
</div>
<!-- 使用说明 -->
<div style="text-align: left; background: #f5f5f5; padding: 12px; border-radius: 4px; margin-bottom: 20px">
<div style="font-weight: 500; margin-bottom: 8px">使用说明</div>
<div style="font-size: 12px; color: #666; line-height: 1.5">
<template v-if="qrCodeType === 'web'">
1. 复制邀请链接发送给用户或让用户扫描网页二维码<br>
2. 用户点击链接或扫码进入注册页面<br>
3. 用户完成注册后系统自动建立推荐关系<br>
4. 您可以在"推荐关系管理"中查看邀请结果
</template>
<template v-else>
1. 让用户扫描小程序码进入小程序<br>
2. 小程序会自动识别邀请信息<br>
3. 用户在小程序内完成注册后系统自动建立推荐关系<br>
4. 您可以在"推荐关系管理"中查看邀请结果
</template>
</div>
</div>
<!-- 调试信息 -->
<div v-if="showDebugInfo" style="margin-bottom: 16px; padding: 8px; background: #f0f0f0; border-radius: 4px; font-size: 12px;">
<div><strong>调试信息:</strong></div>
<div>邀请人ID: {{ inviterId }}</div>
<div>邀请链接: {{ invitationLink }}</div>
<div v-if="qrCodeType === 'miniprogram'">小程序码URL: {{ miniProgramCodeUrl }}</div>
<div>BaseUrl: {{ baseUrl }}</div>
</div>
<!-- 操作按钮 -->
<div>
<a-space>
<a-button @click="downloadQRCode">下载二维码</a-button>
<a-button type="primary" @click="copyLink">复制链接</a-button>
</a-space>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import { useRouter } from 'vue-router';
import { generateInviteCode } from '@/api/miniprogram';
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
visible: boolean;
inviterId?: number; // 邀请人ID当前登录用户ID
}>();
// 二维码类型
const qrCodeType = ref<'web' | 'miniprogram'>('web');
// 小程序码URL
const miniProgramCodeUrl = ref<string>('');
// 小程序码加载状态
const loadingMiniCode = ref(false);
// 显示调试信息
const showDebugInfo = ref(false);
// 基础URL用于调试
const baseUrl = ref('');
// 获取邀请人ID
const inviterId = computed(() => {
return props.inviterId || Number(localStorage.getItem('UserId'));
});
// 生成邀请链接
const invitationLink = computed(() => {
const baseUrl = window.location.origin;
return `${baseUrl}/dealer/register?inviter=${inviterId.value}`;
});
// 复制链接
const copyLink = async () => {
try {
await navigator.clipboard.writeText(invitationLink.value);
message.success('邀请链接已复制到剪贴板');
} catch (e) {
// 降级方案
const textArea = document.createElement('textarea');
textArea.value = invitationLink.value;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
message.success('邀请链接已复制到剪贴板');
}
};
// 加载小程序码
const loadMiniProgramCode = async () => {
const currentInviterId = inviterId.value;
if (!currentInviterId) {
console.error('邀请人ID不存在');
message.error('邀请人ID不存在');
return;
}
console.log('开始加载小程序码邀请人ID:', currentInviterId);
loadingMiniCode.value = true;
try {
const codeUrl = await generateInviteCode(currentInviterId);
console.log('小程序码生成成功:', codeUrl);
miniProgramCodeUrl.value = codeUrl;
message.success('小程序码加载成功');
} catch (e: any) {
console.error('加载小程序码失败:', e);
message.error(`小程序码加载失败: ${e.message}`);
} finally {
loadingMiniCode.value = false;
}
};
// 小程序码加载错误
const onMiniProgramCodeError = () => {
console.error('小程序码图片加载失败');
message.error('小程序码显示失败');
};
// 小程序码加载成功
const onMiniProgramCodeLoad = () => {
console.log('小程序码图片加载成功');
};
// 二维码类型切换
const onQRCodeTypeChange = () => {
if (qrCodeType.value === 'miniprogram' && !miniProgramCodeUrl.value) {
loadMiniProgramCode();
}
};
// 下载二维码
const downloadQRCode = () => {
try {
if (qrCodeType.value === 'web') {
// 下载网页二维码 - 查找SVG元素
const svgElement = document.querySelector('.ant-modal-body svg') as SVGElement;
if (svgElement) {
// 将SVG转换为Canvas再下载
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
// 获取SVG的XML字符串
const svgData = new XMLSerializer().serializeToString(svgElement);
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
canvas.width = img.width || 200;
canvas.height = img.height || 200;
ctx?.drawImage(img, 0, 0);
const link = document.createElement('a');
link.download = `邀请注册二维码.png`;
link.href = canvas.toDataURL('image/png');
link.click();
URL.revokeObjectURL(url);
message.success('二维码已下载');
};
img.onerror = () => {
URL.revokeObjectURL(url);
message.error('二维码下载失败');
};
img.src = url;
} else {
message.error('未找到二维码,请稍后重试');
}
} else {
// 下载小程序码
if (miniProgramCodeUrl.value) {
const link = document.createElement('a');
link.download = `邀请小程序码.png`;
link.href = miniProgramCodeUrl.value;
link.target = '_blank';
link.click();
message.success('小程序码已下载');
} else {
message.error('小程序码未加载');
}
}
} catch (e) {
console.error('下载失败:', e);
message.error('下载失败');
}
};
// 更新visible
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 监听弹窗显示状态
watch(() => props.visible, (visible) => {
if (visible) {
// 重置状态
qrCodeType.value = 'web';
miniProgramCodeUrl.value = '';
loadingMiniCode.value = false;
showDebugInfo.value = false;
// 获取调试信息
import('@/config/setting').then(({ SERVER_API_URL }) => {
baseUrl.value = SERVER_API_URL;
});
}
});
</script>
<style lang="less" scoped>
:deep(.ant-typography-title) {
margin-bottom: 8px !important;
}
:deep(.ant-input-group-addon) {
padding: 0;
}
</style>

View File

@@ -1,71 +0,0 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
allow-clear
mode="multiple"
:value="roleIds"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
>
<a-select-option
v-for="item in data"
:key="item.roleId"
:value="item.roleId"
>
{{ item.roleName }}
</a-select-option>
</a-select>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { message } from 'ant-design-vue/es';
import { listRoles } from '@/api/system/role';
import type { Role } from '@/api/system/role/model';
const emit = defineEmits<{
(e: 'update:value', value: Role[]): void;
(e: 'blur'): void;
}>();
const props = withDefaults(
defineProps<{
// 选中的角色
value?: Role[];
//
placeholder?: string;
}>(),
{
placeholder: '请选择角色'
}
);
// 选中的角色id
const roleIds = computed(() => props.value?.map((d) => d.roleId as number));
// 角色数据
const data = ref<Role[]>([]);
/* 更新选中数据 */
const updateValue = (value: number[]) => {
emit(
'update:value',
value.map((v) => ({ roleId: v }))
);
};
/* 获取角色数据 */
listRoles()
.then((list) => {
data.value = list;
})
.catch((e) => {
message.error(e.message);
});
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -1,51 +0,0 @@
<template>
<a-table :dataSource="dataSource" :columns="columns" :pagination="false">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="flex">
<span class="w-32">{{ record.name }}</span>
<span class="w-32">{{ record.value }}</span>
</div>
</template>
<template v-if="column.key === 'action'">
<template v-if="record.key === '2'">
<a-button>重置</a-button>
</template>
</template>
</template>
</a-table>
</template>
<script>
export default {
setup() {
return {
columns: [
{
title: '开发者ID',
dataIndex: 'name',
key: 'name',
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
align: 'center',
width: 240,
},
],
dataSource: [
{
key: '1',
name: '租户ID',
value: '10550'
},
{
key: '2',
name: 'AppSecret',
value: 'sdfsdfsdfsdfs'
},
],
};
},
};
</script>

View File

@@ -1,45 +0,0 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择性别'
}
);
// 字典数据
const data = getDictionaryOptions('sex');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>

View File

@@ -4,7 +4,7 @@
:width="500"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '编辑员' : '添加员'"
:title="isUpdate ? '编辑项目成员' : '添加项目成员'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
@@ -22,7 +22,6 @@
<a-input
allow-clear
:maxlength="20"
:disabled="isUpdate"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
@@ -36,9 +35,6 @@
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="角色" name="roles">
<role-select v-model:value="form.roles" />
</a-form-item>
<a-form-item v-if="!isUpdate" label="登录密码" name="password">
<a-input-password
:maxlength="20"
@@ -46,22 +42,22 @@
placeholder="请输入登录密码"
/>
</a-form-item>
<a-form-item label="性别" name="sex">
<DictSelect
dict-code="sex"
:placeholder="`请选择性别`"
v-model:value="form.sexName"
@done="chooseSex"
/>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入邮箱"
v-model:value="form.email"
/>
</a-form-item>
<!-- <a-form-item label="性别" name="sex">-->
<!-- <DictSelect-->
<!-- dict-code="sex"-->
<!-- :placeholder="`请选择性别`"-->
<!-- v-model:value="form.sexName"-->
<!-- @done="chooseSex"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="邮箱" name="email">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- :maxlength="100"-->
<!-- placeholder="请输入邮箱"-->
<!-- v-model:value="form.email"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="所属机构" name="type">
<org-select
:data="organizationList"
@@ -81,13 +77,10 @@
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import RoleSelect from './role-select.vue';
import { addUser, updateUser, checkExistence } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
import OrgSelect from './org-select.vue';
// import { getDictionaryOptions } from '@/utils/common';
import { Organization } from '@/api/system/organization/model';
import { Grade } from '@/api/user/grade/model';
import {TEMPLATE_ID} from "@/config/setting";
// 是否开启响应式布局
@@ -183,14 +176,6 @@
trigger: 'blur'
}
],
// sex: [
// {
// required: true,
// message: '请选择性别',
// type: 'string',
// trigger: 'blur'
// }
// ],
roles: [
{
required: true,
@@ -231,20 +216,11 @@
]
});
const chooseGradeId = (data: Grade) => {
form.gradeName = data.name;
form.gradeId = data.gradeId;
};
const chooseSex = (data: any) => {
form.sex = data.key;
form.sexName = data.label;
};
const updateIsAdmin = (value: boolean) => {
form.isAdmin = value;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {

View File

@@ -1,143 +0,0 @@
<!-- 用户编辑弹窗 -->
<template>
<a-drawer
:width="680"
:visible="visible"
:confirm-loading="loading"
:title="'基本信息'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="账号">
<span class="ele-text">{{ user.username }}</span>
</a-form-item>
<a-form-item label="昵称">
<span class="ele-text">{{ user.nickname }}</span>
</a-form-item>
<a-form-item label="性别">
<span class="ele-text">{{ user.sexName }}</span>
</a-form-item>
<a-form-item label="手机号">
<span class="ele-text">{{ user.phone }}</span>
</a-form-item>
<a-form-item label="角色">
<a-tag v-for="item in user.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</a-form-item>
<a-form-item label="状态">
<a-badge
v-if="typeof user.status === 'number'"
:status="(['processing', 'error'][user.status] as any)"
:text="['正常', '冻结'][user.status]"
/>
</a-form-item>
<a-form-item label="地址">
<span class="ele-text">{{ user.address }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="可用余额">
<span class="ele-text-success">{{ formatNumber(user.balance) }}</span>
</a-form-item>
<a-form-item label="可用积分">
<span class="ele-text">{{ user.points }}</span>
</a-form-item>
<a-form-item label="实际消费">
<span class="ele-text">{{ user.payMoney }}</span>
</a-form-item>
<a-form-item label="机构/部门">
<span class="ele-text">{{ user.organizationName }}</span>
</a-form-item>
<a-form-item label="头像">
<a-image :src="user.avatar" :width="36" />
</a-form-item>
<a-form-item label="生日">
<span class="ele-text">{{ user.birthday }}</span>
</a-form-item>
<a-form-item label="创建时间">
<span class="ele-text">{{ user.createTime }}</span>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import type { User } from '@/api/system/user/model';
import { useThemeStore } from '@/store/modules/theme';
import { formatNumber } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: User | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const user = reactive<User>({
username: '',
nickname: '',
sexName: '',
phone: '',
avatar: '',
balance: undefined,
points: 0,
payMoney: 0,
birthday: '',
address: '',
roles: [],
createTime: undefined,
status: undefined
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(user);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(user, props.data);
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -1,111 +0,0 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
"
>
<a-row :gutter="8">
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="用户账号">
<a-input
v-model:value.trim="form.username"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="昵称">
<a-input
v-model:value.trim="form.nickname"
placeholder="请输入"
allow-clear
/>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item label="性别">
<a-select v-model:value="form.sex" placeholder="请选择" allow-clear>
<a-select-option value="1">男</a-select-option>
<a-select-option value="2">女</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
: { span: 6 }
"
>
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { UserParam } from '@/api/system/user/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 默认搜索条件
where?: UserParam;
}>();
const emit = defineEmits<{
(e: 'search', where?: UserParam): void;
}>();
// 表单数据
const { form, resetFields } = useFormData<UserParam>({
username: '',
nickname: '',
sex: undefined,
...props.where
});
/* 搜索 */
const search = () => {
emit('search', form);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
</script>

View File

@@ -16,11 +16,11 @@
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<a-button type="primary" class="ele-btn-icon" @click="openInvitation">
<template #icon>
<plus-outlined/>
</template>
<span>添加</span>
<span>邀请注册</span>
</a-button>
<a-input-search
allow-clear
@@ -43,12 +43,12 @@
</template>
</a-avatar>
</template>
<template v-if="column.key === 'nickname'">
<span>{{ record.nickname }}</span>
</template>
<template v-if="column.key === 'mobile'">
<span v-if="hasRole('superAdmin')">{{ record.phone }}</span>
<span v-else>{{ record.mobile }}</span>
<template v-if="column.key === 'realName'">
<div class="flex flex-col items-center">
<span>{{ record.realName }}</span>
<span class="text-gray-400" v-if="hasRole('superAdmin')">{{ record.phone }}</span>
<span class="text-gray-400" v-else>{{ record.mobile }}</span>
</div>
</template>
<template v-if="column.key === 'roles'">
<a-tag v-for="item in record.roles" :key="item.roleId" color="blue">
@@ -105,6 +105,8 @@
<user-import v-model:visible="showImport" @done="reload"/>
<!-- 用户详情 -->
<user-info v-model:visible="showInfo" :data="current" @done="reload"/>
<!-- 邀请注册弹窗 -->
<invitation-modal v-model:visible="showInvitation" :inviter-id="currentUserId"/>
</a-page-header>
</template>
@@ -126,13 +128,11 @@ import type {
} from 'ele-admin-pro/es/ele-pro-table/types';
import {messageLoading, formatNumber} from 'ele-admin-pro/es';
import UserEdit from './components/user-edit.vue';
import UserImport from './components/user-import.vue';
import UserInfo from './components/user-info.vue';
import InvitationModal from './components/invitation-modal.vue';
import {toDateString} from 'ele-admin-pro';
import {
pageUsers,
removeUser,
removeUsers,
updateUserPassword,
updateUser
} from '@/api/system/user';
@@ -142,10 +142,9 @@ import {listRoles} from '@/api/system/role';
import {listOrganizations} from '@/api/system/organization';
import {Organization} from '@/api/system/organization/model';
import {hasRole} from '@/utils/permission';
import {getPageTitle, push} from "@/utils/common";
import {getPageTitle} from "@/utils/common";
import router from "@/router";
import SuperAdmin from './components/super-admin.vue';
import Admin from './components/admin.vue';
// 加载状态
const loading = ref(true);
@@ -165,8 +164,11 @@ const showEdit = ref(false);
const showInfo = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
const userType = ref<number>();
// 是否显示邀请注册弹窗
const showInvitation = ref(false);
const searchText = ref('');
// 当前用户ID
const currentUserId = ref<number>();
// 加载角色
const roles = ref<any[]>([]);
@@ -217,19 +219,7 @@ const columns = ref<ColumnItem[]>([
{
title: '真实姓名',
dataIndex: 'realName',
align: 'center',
showSorterTooltip: false
},
{
title: '手机号码',
dataIndex: 'mobile',
align: 'center',
key: 'mobile',
showSorterTooltip: false
},
{
title: '性别',
dataIndex: 'sexName',
key: 'realName',
align: 'center',
showSorterTooltip: false
},
@@ -252,7 +242,7 @@ const columns = ref<ColumnItem[]>([
align: 'center',
showSorterTooltip: false,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd')
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
@@ -296,22 +286,6 @@ const openEdit = (row?: User) => {
showEdit.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: User) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 打开编辑弹窗 */
const openImport = () => {
showImport.value = true;
};
const handleTabs = (e) => {
userType.value = Number(e.target.value);
reload();
};
/* 删除单个 */
const remove = (row: User) => {
const hide = messageLoading('请求中..', 0);
@@ -327,33 +301,6 @@ const remove = (row: User) => {
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的用户吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeUsers(selection.value.map((d) => d.userId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 重置用户密码 */
const resetPsw = (row: User) => {
Modal.confirm({
@@ -410,6 +357,18 @@ const query = async () => {
}
}
/* 打开邀请注册弹窗 */
const openInvitation = () => {
// 获取当前用户ID
const userId = localStorage.getItem('UserId');
if (userId) {
currentUserId.value = Number(userId);
showInvitation.value = true;
} else {
message.error('获取用户信息失败');
}
};
watch(
() => router.currentRoute.value.query,
() => {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,159 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
<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.dealerName"
placeholder="请输入客户名称"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="联系电话">
<a-input
v-model:value="searchForm.mobile"
placeholder="客户联系电话"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="审核状态">
<a-select
v-model:value="searchForm.applyStatus"
placeholder="全部状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">跟进中</a-select-option>
<a-select-option :value="20">已签约</a-select-option>
<a-select-option :value="30">已取消</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="添加时间">
<a-range-picker
v-model:value="searchForm.dateRange"
style="width: 240px"
/>
</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>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import { reactive } from 'vue';
import {
SearchOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerApplyParam } from '@/api/shop/shopDealerApply/model';
import dayjs from 'dayjs';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
// 选中的数据
selection?: any[];
}>(),
{}
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'search', where?: ShopDealerApplyParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'batchApprove'): void;
(e: 'export'): void;
}>();
// 新增
const add = () => {
emit('add');
// 搜索表单
const searchForm = reactive<any>({
realName: '',
mobile: '',
applyType: undefined,
applyStatus: undefined,
dateRange: undefined
});
// 搜索
const handleSearch = () => {
const searchParams: ShopDealerApplyParam = {};
if (searchForm.realName) {
searchParams.realName = searchForm.realName;
}
if (searchForm.mobile) {
searchParams.mobile = searchForm.mobile;
}
if (searchForm.applyType) {
searchParams.applyType = searchForm.applyType;
}
if (searchForm.applyStatus) {
searchParams.applyStatus = searchForm.applyStatus;
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
searchParams.startTime = dayjs(searchForm.dateRange[0]).format('YYYY-MM-DD');
searchParams.endTime = dayjs(searchForm.dateRange[1]).format('YYYY-MM-DD');
}
if(searchForm.dealerName){
searchParams.dealerName = searchForm.dealerName;
}
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
searchForm.realName = '';
searchForm.mobile = '';
searchForm.applyType = undefined;
searchForm.applyStatus = undefined;
searchForm.dateRange = undefined;
emit('search', {});
};
watch(
() => props.selection,
() => {}
);
</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

@@ -1,11 +1,11 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销商申请记录表' : '添加分销商申请记录表'"
:title="isUpdate ? '编辑客户' : '新增客户'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -14,81 +14,197 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="姓名" name="realName">
<a-input
allow-clear
placeholder="请输入姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
placeholder="请输入手机号"
v-model:value="form.mobile"
/>
</a-form-item>
<a-form-item label="推荐人用户ID" name="refereeId">
<a-input
allow-clear
placeholder="请输入推荐人用户ID"
v-model:value="form.refereeId"
/>
</a-form-item>
<a-form-item label="申请方式(10需后台审核 20无需审核)" name="applyType">
<a-input
allow-clear
placeholder="请输入申请方式(10需后台审核 20无需审核)"
v-model:value="form.applyType"
/>
</a-form-item>
<a-form-item label="申请时间" name="applyTime">
<a-input
allow-clear
placeholder="请输入申请时间"
v-model:value="form.applyTime"
/>
</a-form-item>
<a-form-item label="审核状态 (10待审核 20审核通过 30驳回)" name="applyStatus">
<a-input
allow-clear
placeholder="请输入审核状态 (10待审核 20审核通过 30驳回)"
v-model:value="form.applyStatus"
/>
</a-form-item>
<a-form-item label="审核时间" name="auditTime">
<a-input
allow-clear
placeholder="请输入审核时间"
v-model:value="form.auditTime"
/>
</a-form-item>
<a-form-item label="驳回原因" name="rejectReason">
<a-input
allow-clear
placeholder="请输入驳回原因"
v-model:value="form.rejectReason"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
<!-- 客户信息 -->
<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="dealerName">
<a-input
placeholder="请输入客户名称"
v-model:value="form.dealerName"
:disabled="form.applyStatus == 20"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系人" name="realName">
<a-input
placeholder="请输入联系人"
v-model:value="form.realName"
:disabled="form.applyStatus == 20"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="手机号码" name="mobile">
<a-input
placeholder="请输入手机号码"
:disabled="form.applyStatus == 20"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="收益基数" name="rate">
<a-input-number
placeholder="0.007"
:min="0"
:max="1"
step="0.01"
:disabled="!hasRole('superAdmin')"
v-model:value="form.rate"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 报备人信息 -->
<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"
:disabled="isUpdate"
v-model:value="form.userId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="报备人" name="nickName">
<a-input-number
:min="1"
placeholder="请输入报备人名称"
:disabled="isUpdate"
v-model:value="form.nickName"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="推荐人ID" name="refereeId">
<a-input-number
:min="1"
placeholder="请输入推荐人用户ID"
:disabled="isUpdate"
v-model:value="form.refereeId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="推荐人名称" name="refereeName">
<a-input-number
:min="1"
placeholder="请输入推荐人名称"
:disabled="isUpdate"
v-model:value="form.refereeName"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 审核信息 -->
<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="applyStatus">
<a-select v-model:value="form.applyStatus" placeholder="请选择审核状态" @change="handleStatusChange">
<a-select-option :value="10">
<a-tag color="orange">跟进中</a-tag>
<span style="margin-left: 8px;">正在跟进中</span>
</a-select-option>
<a-select-option :value="20">
<a-tag color="success">已签约</a-tag>
<span style="margin-left: 8px;">客户已签约</span>
</a-select-option>
<a-select-option :value="30">
<a-tag color="error">已取消</a-tag>
<span style="margin-left: 8px;">客户已取消</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12" v-if="form.applyStatus === 30">
<a-form-item label="取消原因" name="rejectReason">
<a-textarea
v-model:value="form.rejectReason"
placeholder="请输入取消原因"
style="width: 100%"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
</a-col>
</a-row>
<!-- 跟进情况 -->
<a-divider orientation="left" v-if="form.applyStatus == 10">
<span style="color: #1890ff; font-weight: 600;">跟进情况</span>
</a-divider>
<!-- 历史跟进记录 -->
<div v-if="form.applyStatus == 10 && historyRecords.length > 0">
<a-divider orientation="left" style="font-size: 14px; color: #666;">
历史跟进记录
</a-divider>
<div v-for="(record, index) in historyRecords" :key="record.id" style="margin-bottom: 16px; padding: 12px; background-color: #f5f5f5; border-radius: 4px;">
<div class="flex justify-between" style="font-weight: 500; margin-bottom: 8px;">
<div>跟进 #{{ historyRecords.length - index }} <span class="text-gray-400 px-4">{{ record.createTime }}</span></div>
<a-tag color="#f50" class="cursor-pointer" @click="remove(record)">删除</a-tag>
</div>
<div style="white-space: pre-wrap;">
{{ record.content }}
</div>
</div>
</div>
<!-- 新增跟进记录 -->
<div v-if="form.applyStatus == 10">
<a-divider orientation="left" style="font-size: 14px; color: #666;">
新增跟进记录
</a-divider>
<a-form-item :wrapper-col="{ span: 24 }">
<div class="flex flex-col gap-2">
<a-textarea
v-model:value="newFollowUpContent"
placeholder="请输入本次跟进内容"
:rows="4"
:maxlength="500"
style="width: 80%"
show-count
/>
<div class="btn">
<a-button
type="primary"
@click="saveFollowUpRecord"
:loading="followUpLoading"
:disabled="!newFollowUpContent.trim()"
>
保存跟进记录
</a-button>
</div>
</div>
</a-form-item>
</div>
</a-form>
</ele-modal>
</template>
@@ -96,21 +212,19 @@
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import dayjs from 'dayjs';
import { assignObject } from 'ele-admin-pro';
import { addShopDealerApply, updateShopDealerApply } from '@/api/shop/shopDealerApply';
import {listShopDealerRecord, addShopDealerRecord, removeShopDealerRecord} from '@/api/shop/shopDealerRecord';
import { ShopDealerApply } from '@/api/shop/shopDealerApply/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';
import { ShopDealerRecord } from '@/api/shop/shopDealerRecord/model';
import { FormInstance, RuleObject } from 'ant-design-vue/es/form';
import { messageLoading } from 'ele-admin-pro';
import {hasRole} from "@/utils/permission";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
@@ -126,32 +240,38 @@
// 提交状态
const loading = ref(false);
// 跟进记录保存状态
const followUpLoading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
// 历史跟进记录
const historyRecords = ref<ShopDealerRecord[]>([]);
// 新的跟进内容
const newFollowUpContent = ref('');
// 表单数据
const form = reactive<ShopDealerApply>({
applyId: undefined,
userId: undefined,
realName: undefined,
mobile: undefined,
nickName: undefined,
realName: '',
mobile: '',
dealerName: '',
rate: 0.007,
refereeId: undefined,
applyType: undefined,
refereeName: undefined,
applyType: 10,
applyTime: undefined,
applyStatus: undefined,
applyStatus: 10,
auditTime: undefined,
rejectReason: undefined,
rejectReason: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopDealerApplyId: undefined,
shopDealerApplyName: '',
status: 0,
comments: '',
sortNumber: 100
updateTime: undefined
});
/* 更新visible */
@@ -161,77 +281,281 @@
// 表单验证规则
const rules = reactive({
shopDealerApplyName: [
userId: [
{
required: true,
type: 'string',
message: '请填写分销商申请记录表名称',
message: '请输入用户ID',
trigger: 'blur'
}
} as RuleObject
],
realName: [
{
required: true,
message: '请输入客户名称',
trigger: 'blur'
} as RuleObject,
{
min: 2,
max: 20,
message: '姓名长度应在2-20个字符之间',
trigger: 'blur'
} as RuleObject
],
rate: [
{
required: true,
message: '请输入收益基数',
trigger: 'blur'
} as RuleObject
],
mobile: [
{
required: true,
message: '请输入手机号码',
trigger: 'blur'
} as RuleObject,
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
} as RuleObject
],
applyType: [
{
required: true,
message: '请选择申请方式',
trigger: 'change'
} as RuleObject
],
applyStatus: [
{
required: true,
message: '请选择审核状态',
trigger: 'change'
} as RuleObject
],
rejectReason: [
{
required: true,
message: '驳回时必须填写驳回原因',
trigger: 'blur'
} as RuleObject
],
auditTime: [
{
required: true,
message: '审核时请选择审核时间',
trigger: 'change'
} as RuleObject
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
/* 获取历史跟进记录 */
const fetchHistoryRecords = async (dealerId: number) => {
try {
// 先通过list接口获取所有记录然后过滤
const allRecords = await listShopDealerRecord({});
const records = allRecords.filter(record => record.dealerId === dealerId);
// 按创建时间倒序排列(最新的在前面)
historyRecords.value = records.sort((a, b) => {
if (a.createTime && b.createTime) {
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime();
}
return 0;
});
} catch (error) {
console.error('获取历史跟进记录失败:', error);
message.error('获取历史跟进记录失败');
historyRecords.value = [];
}
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
/* 保存新的跟进记录 */
const saveFollowUpRecord = async () => {
// 检查是否有客户ID
if (!form.applyId) {
message.warning('请先保存客户信息');
return;
}
// 检查是否有跟进内容
if (!newFollowUpContent.value.trim()) {
message.warning('请输入跟进内容');
return;
}
try {
followUpLoading.value = true;
const recordData: any = {
content: newFollowUpContent.value.trim(),
dealerId: form.applyId,
userId: form.userId,
status: 1, // 默认设置为已完成
sortNumber: 100
};
// 新增逻辑
await addShopDealerRecord(recordData);
message.success('跟进记录保存成功');
// 保存最后跟进内容到主表
await updateShopDealerApply({
...form,
comments: newFollowUpContent.value.trim()
})
// 清空输入框
newFollowUpContent.value = '';
// 重新加载历史记录
await fetchHistoryRecords(form.applyId);
} catch (error) {
console.error('保存跟进记录失败:', error);
message.error('保存跟进记录失败');
} finally {
followUpLoading.value = false;
}
};
const { resetFields } = useForm(form, rules);
/* 处理审核状态变化 */
const handleStatusChange = (value: number) => {
// 当状态改为审核通过或驳回时,自动设置审核时间为当前时间
if ((value === 20 || value === 30) && !form.auditTime) {
form.auditTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
}
// 当状态改为待审核时,清空审核时间和驳回原因
if (value === 10) {
form.auditTime = undefined;
form.rejectReason = '';
}
};
/* 删除单个 */
const remove = (row: ShopDealerRecord) => {
const hide = messageLoading('请求中..', 0);
removeShopDealerRecord(row.id)
.then((msg) => {
hide();
message.success(msg);
// 重新加载历史记录
if(props.data?.applyId){
fetchHistoryRecords(props.data?.applyId);
}
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
// 动态验证规则
const validateFields: string[] = ['userId', 'realName', 'mobile', 'applyStatus'];
// 如果是驳回状态,需要验证驳回原因
if (form.applyStatus === 30) {
validateFields.push('rejectReason');
}
// 如果是审核通过或驳回状态,需要验证审核时间
if (form.applyStatus === 20 || form.applyStatus === 30) {
validateFields.push('auditTime');
}
formRef.value
.validate()
.then(() => {
.validate(validateFields)
.then(async () => {
loading.value = true;
const formData = {
...form
};
// 处理时间字段转换 - 转换为ISO字符串格式
if (formData.applyTime) {
if (dayjs.isDayjs(formData.applyTime)) {
formData.applyTime = formData.applyTime.format('YYYY-MM-DD HH:mm:ss');
} else if (typeof formData.applyTime === 'number') {
formData.applyTime = dayjs(formData.applyTime).format('YYYY-MM-DD HH:mm:ss');
}
}
if (formData.auditTime) {
if (dayjs.isDayjs(formData.auditTime)) {
formData.auditTime = formData.auditTime.format('YYYY-MM-DD HH:mm:ss');
} else if (typeof formData.auditTime === 'number') {
formData.auditTime = dayjs(formData.auditTime).format('YYYY-MM-DD HH:mm:ss');
}
}
// 当审核状态为通过或驳回时,确保有审核时间
if ((formData.applyStatus === 20 || formData.applyStatus === 30) && !formData.auditTime) {
formData.auditTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
}
// 当状态为待审核时,清空审核时间
if (formData.applyStatus === 10) {
formData.auditTime = undefined;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerApply : addShopDealerApply;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
try {
const msg = await saveOrUpdate(formData);
message.success(msg);
updateVisible(false);
emit('done');
} catch (e: any) {
message.error(e.message);
} finally {
loading.value = false;
}
})
.catch(() => {});
.catch(() => {
loading.value = false;
});
};
watch(
() => props.visible,
(visible) => {
async (visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
// 如果是修改且状态为跟进中,获取历史跟进记录
if (props.data.applyId && props.data.applyStatus === 10) {
await fetchHistoryRecords(props.data.applyId);
}
} else {
// 重置为默认值
Object.assign(form, {
applyId: undefined,
userId: undefined,
realName: '',
mobile: '',
refereeId: undefined,
applyType: 10,
applyTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
isUpdate.value = false;
// 重置历史记录和新内容
historyRecords.value = [];
newFollowUpContent.value = '';
}
} else {
resetFields();
@@ -240,3 +564,32 @@
{ immediate: true }
);
</script>
<style lang="less" scoped>
: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-radio) {
display: flex;
align-items: center;
margin-bottom: 8px;
.ant-radio-inner {
margin-right: 8px;
}
}
:deep(.ant-select-selection-item) {
display: flex;
align-items: center;
}
</style>

View File

@@ -1,274 +1,440 @@
<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="shopDealerApplyId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="applyId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@batchApprove="batchApprove"
@export="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applyStatus'">
<a-tag v-if="record.applyStatus === 10" color="orange">跟进中</a-tag>
<a-tag v-if="record.applyStatus === 20" color="green">已签约</a-tag>
<a-tag v-if="record.applyStatus === 30" color="red">已取消</a-tag>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
<template v-if="column.key === 'customer'">
<div class="flex flex-col">
<span class="font-medium">{{ record.dealerName }}</span>
<span class="text-gray-400">联系人{{ record.realName }}</span>
<span class="text-gray-400">联系电话{{ record.mobile }}</span>
<span class="text-gray-400">户号{{ record.dealerCode }}</span>
</div>
</template>
<template v-if="column.key === 'applicantInfo'">
<div class="text-gray-600">{{ record.nickName }}</div>
<div class="text-gray-400">{{ record.phone }}</div>
<div class="text-gray-400">{{ record.rate }}</div>
</template>
<template v-if="column.key === 'comments'">
<div class="text-gray-400">{{ record.comments }}</div>
<div class="text-gray-400" v-if="record.comments">{{ record.updateTime }}</div>
</template>
<template v-if="column.key === 'createTime'">
<div class="flex flex-col">
<span>{{ record.createTime }}</span>
<span>保护期7</span>
</div>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)" class="ele-text-primary">
<EditOutlined/>
编辑
</a>
<template v-if="record.applyStatus !== 20">
<a-divider type="vertical"/>
<a @click="approveApply(record)" class="ele-text-success">
<CheckOutlined/>
已签约
</a>
<a-divider type="vertical"/>
<a @click="rejectApply(record)" class="ele-text-warning">
<CloseOutlined/>
驳回
</a>
<a-divider type="vertical"/>
<a-popconfirm
v-if="record.applyStatus != 20"
title="确定要删除此申请记录吗?"
@confirm="remove(record)"
placement="topRight"
>
<a class="ele-text-danger">
<DeleteOutlined/>
删除
</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopDealerApplyEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 编辑弹窗 -->
<ShopDealerApplyEdit 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 } 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 ShopDealerApplyEdit from './components/shopDealerApplyEdit.vue';
import { pageShopDealerApply, removeShopDealerApply, removeBatchShopDealerApply } from '@/api/shop/shopDealerApply';
import type { ShopDealerApply, ShopDealerApplyParam } from '@/api/shop/shopDealerApply/model';
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {
ExclamationCircleOutlined,
CheckOutlined,
CloseOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} 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 ShopDealerApplyEdit from './components/shopDealerApplyEdit.vue';
import {
pageShopDealerApply,
removeShopDealerApply,
removeBatchShopDealerApply,
batchApproveShopDealerApply,
updateShopDealerApply
} from '@/api/shop/shopDealerApply';
import type {ShopDealerApply, ShopDealerApplyParam} from '@/api/shop/shopDealerApply/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerApply[]>([]);
// 当前编辑数据
const current = ref<ShopDealerApply | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格选中数据
const selection = ref<ShopDealerApply[]>([]);
// 当前编辑数据
const current = ref<ShopDealerApply | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 4;
return pageShopDealerApply({
...where,
...orders,
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 90,
fixed: 'left'
},
{
title: '客户名称',
dataIndex: 'customer',
key: 'customer'
},
{
title: '最后跟进情况',
dataIndex: 'comments',
key: 'comments',
align: 'left'
},
// {
// title: '收益基数',
// dataIndex: 'rate',
// key: 'rate',
// align: 'left'
// },
{
title: '报备人信息',
dataIndex: 'applicantInfo',
key: 'applicantInfo',
align: 'left',
fixed: 'left',
customRender: ({record}) => {
return `${record.nickName || '-'} (${record.phone || '-'})`;
}
return pageShopDealerApply({
...where,
...orders,
page,
limit
});
};
},
{
title: '状态',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
width: 120
},
// {
// title: '申请时间',
// dataIndex: 'applyTime',
// key: 'applyTime',
// align: 'center',
// width: 120,
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// },
// {
// title: '审核时间',
// dataIndex: 'auditTime',
// key: 'auditTime',
// align: 'center',
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// },
{
title: '添加时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true
}
// {
// title: '操作',
// key: 'action',
// fixed: 'right',
// align: 'center',
// width: 380,
// hideInSetting: true
// }
]);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
dataIndex: 'applyId',
key: 'applyId',
align: 'center',
width: 90,
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '姓名',
dataIndex: 'realName',
key: 'realName',
align: 'center',
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile',
align: 'center',
},
{
title: '推荐人用户ID',
dataIndex: 'refereeId',
key: 'refereeId',
align: 'center',
},
{
title: '申请方式(10需后台审核 20无需审核)',
dataIndex: 'applyType',
key: 'applyType',
align: 'center',
},
{
title: '申请时间',
dataIndex: 'applyTime',
key: 'applyTime',
align: 'center',
},
{
title: '审核状态 (10待审核 20审核通过 30驳回)',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
},
{
title: '审核时间',
dataIndex: 'auditTime',
key: 'auditTime',
align: 'center',
},
{
title: '驳回原因',
dataIndex: 'rejectReason',
key: 'rejectReason',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopDealerApplyParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 搜索 */
const reload = (where?: ShopDealerApplyParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerApply) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerApply) => {
const hide = message.loading('请求中..', 0);
removeShopDealerApply(row.shopDealerApplyId)
.then((msg) => {
/* 审核通过 */
const approveApply = (row: ShopDealerApply) => {
Modal.confirm({
title: '审核通过确认',
content: `确定要通过 ${row.realName} 的经销商申请吗?`,
icon: createVNode(CheckOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
const hide = message.loading('正在处理审核...', 0);
try {
await updateShopDealerApply({
...row,
applyId: row.applyId,
applyStatus: 20
});
hide();
message.success(msg);
message.success('审核通过成功');
reload();
})
.catch((e) => {
} catch (error: any) {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
message.error(error.message || '审核失败,请重试');
}
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerApply(selection.value.map((d) => d.shopDealerApplyId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
});
};
/* 审核驳回 */
const rejectApply = (row: ShopDealerApply) => {
let rejectReason = '';
Modal.confirm({
title: '审核驳回',
content: createVNode('div', null, [
createVNode('p', null, `申请人: ${row.realName} (${row.mobile})`),
createVNode('p', {style: 'margin-top: 12px;'}, '请输入驳回原因:'),
createVNode('textarea', {
placeholder: '请输入驳回原因...',
style: 'width: 100%; height: 80px; margin-top: 8px; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
onInput: (e: any) => {
rejectReason = e.target.value;
}
})
]),
icon: createVNode(CloseOutlined),
okText: '确认驳回',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
if (!rejectReason.trim()) {
message.error('请输入驳回原因');
return Promise.reject();
}
const hide = message.loading('正在处理审核...', 0);
try {
await updateShopDealerApply({
...row,
applyStatus: 30,
rejectReason: rejectReason.trim()
});
hide();
message.success('审核驳回成功');
reload();
} catch (error: any) {
hide();
message.error(error.message || '审核失败,请重试');
}
}
});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerApply) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerApply) => {
if (!row.applyId) {
message.error('删除失败:缺少必要参数');
return;
}
const hide = message.loading('正在删除申请记录...', 0);
removeShopDealerApply(row.applyId)
.then((msg) => {
hide();
message.success(msg || '删除成功');
reload();
})
.catch((e) => {
hide();
message.error(e.message || '删除失败');
});
};
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
/* 自定义行属性 */
const customRow = (record: ShopDealerApply) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
const validIds = selection.value.filter(d => d.applyId).map(d => d.applyId);
if (!validIds.length) {
message.error('选中的数据中没有有效的ID');
return;
}
Modal.confirm({
title: '批量删除确认',
content: `确定要删除选中的 ${validIds.length} 条申请记录吗?此操作不可恢复。`,
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk: () => {
const hide = message.loading(`正在删除 ${validIds.length} 条记录...`, 0);
removeBatchShopDealerApply(validIds)
.then((msg) => {
hide();
message.success(msg || `成功删除 ${validIds.length} 条记录`);
selection.value = [];
reload();
})
.catch((e) => {
hide();
message.error(e.message || '批量删除失败');
});
}
});
};
/* 批量通过 */
const batchApprove = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const pendingApplies = selection.value.filter(item => item.applyStatus === 10);
if (!pendingApplies.length) {
message.error('所选申请中没有待审核的记录');
return;
}
Modal.confirm({
title: '批量通过确认',
content: `确定要通过选中的 ${pendingApplies.length} 个申请吗?`,
icon: createVNode(ExclamationCircleOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
const hide = message.loading('正在批量通过...', 0);
try {
const ids = pendingApplies.map(item => item.applyId);
await batchApproveShopDealerApply(ids);
hide();
message.success(`成功通过 ${pendingApplies.length} 个申请`);
selection.value = [];
reload();
} catch (error: any) {
hide();
message.error(error.message || '批量审核失败,请重试');
}
};
}
});
};
/* 导出数据 */
const exportData = () => {
const hide = message.loading('正在导出申请数据...', 0);
// 这里调用导出API
setTimeout(() => {
hide();
message.success('申请数据导出成功');
}, 2000);
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerApply) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
query();
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerApply'
};
export default {
name: 'ShopDealerApply'
};
</script>
<style lang="less" scoped></style>

View File

@@ -1,11 +1,11 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销商资金明细表' : '添加分销商资金明细表'"
:title="isUpdate ? '编辑资金流动记录' : '新增资金流动记录'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -14,60 +14,128 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="分销商用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入分销商用户ID"
v-model:value="form.userId"
<!-- 基本信息 -->
<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-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="flowType">
<a-select v-model:value="form.flowType" placeholder="请选择资金流动类型">
<a-select-option :value="10">
<div class="flow-type-option">
<a-tag color="success">佣金收入</a-tag>
<span>获得分销佣金</span>
</div>
</a-select-option>
<a-select-option :value="20">
<div class="flow-type-option">
<a-tag color="warning">提现支出</a-tag>
<span>申请提现</span>
</div>
</a-select-option>
<a-select-option :value="30">
<div class="flow-type-option">
<a-tag color="error">转账支出</a-tag>
<span>转账给他人</span>
</div>
</a-select-option>
<a-select-option :value="40">
<div class="flow-type-option">
<a-tag color="processing">转账收入</a-tag>
<span>收到转账</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="金额" name="money">
<a-input-number
:min="0"
:precision="2"
placeholder="请输入金额"
v-model:value="form.money"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="流动描述" name="comments">
<a-textarea
v-model:value="form.comments"
placeholder="请输入资金流动描述"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
<a-form-item label="订单ID" name="orderId">
<a-input
allow-clear
placeholder="请输入订单ID"
v-model:value="form.orderId"
/>
</a-form-item>
<a-form-item label="资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入)" name="flowType">
<a-input
allow-clear
placeholder="请输入资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入)"
v-model:value="form.flowType"
/>
</a-form-item>
<a-form-item label="金额" name="money">
<a-input
allow-clear
placeholder="请输入金额"
v-model:value="form.money"
/>
</a-form-item>
<a-form-item label="描述" name="describe">
<a-input
allow-clear
placeholder="请输入描述"
v-model:value="form.describe"
/>
</a-form-item>
<a-form-item label="对方用户ID" name="toUserId">
<a-input
allow-clear
<!-- 关联信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">关联信息</span>
</a-divider>
<a-form-item
label="对方用户ID"
name="toUserId"
v-if="form.flowType === 30 || form.flowType === 40"
>
<a-input-number
:min="1"
placeholder="请输入对方用户ID"
v-model:value="form.toUserId"
style="width: 300px"
/>
<span style="margin-left: 12px; color: #999; font-size: 12px;">
转账相关操作需要填写对方用户ID
</span>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
<!-- 金额预览 -->
<div class="amount-preview" v-if="form.money && form.flowType">
<a-alert
:type="getAmountAlertType()"
:message="getAmountPreviewText()"
show-icon
style="margin-top: 16px"
/>
</a-form-item>
</div>
</a-form>
</ele-modal>
</template>
@@ -111,23 +179,18 @@
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
// 表单数据
const form = reactive<ShopDealerCapital>({
id: undefined,
userId: undefined,
orderId: undefined,
flowType: undefined,
money: undefined,
describe: undefined,
comments: '',
toUserId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopDealerCapitalId: undefined,
shopDealerCapitalName: '',
status: 0,
comments: '',
sortNumber: 100
updateTime: undefined
});
/* 更新visible */
@@ -137,28 +200,94 @@
// 表单验证规则
const rules = reactive({
shopDealerCapitalName: [
userId: [
{
required: true,
type: 'string',
message: '请填写分销商资金明细表名称',
message: '请输入分销商用户ID',
trigger: 'blur'
}
],
flowType: [
{
required: true,
message: '请选择资金流动类型',
trigger: 'change'
}
],
money: [
{
required: true,
message: '请输入金额',
trigger: 'blur'
},
{
validator: (rule: any, value: any) => {
if (value && value <= 0) {
return Promise.reject('金额必须大于0');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
comments: [
{
required: true,
message: '请输入流动描述',
trigger: 'blur'
},
{
min: 2,
max: 200,
message: '描述长度应在2-200个字符之间',
trigger: 'blur'
}
],
toUserId: [
{
validator: (rule: any, value: any) => {
if ((form.flowType === 30 || form.flowType === 40) && !value) {
return Promise.reject('转账操作必须填写对方用户ID');
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
/* 获取金额预览提示类型 */
const getAmountAlertType = () => {
if (!form.flowType) return 'info';
switch (form.flowType) {
case 10: // 佣金收入
case 40: // 转账收入
return 'success';
case 20: // 提现支出
case 30: // 转账支出
return 'warning';
default:
return 'info';
}
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
/* 获取金额预览文本 */
const getAmountPreviewText = () => {
if (!form.money || !form.flowType) return '';
const amount = parseFloat(form.money.toString()).toFixed(2);
const flowTypeMap = {
10: '佣金收入',
20: '提现支出',
30: '转账支出',
40: '转账收入'
};
const flowTypeName = flowTypeMap[form.flowType] || '未知类型';
const symbol = form.flowType === 10 || form.flowType === 40 ? '+' : '-';
return `${flowTypeName}${symbol}¥${amount}`;
};
const { resetFields } = useForm(form, rules);
@@ -195,18 +324,23 @@
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
id: undefined,
userId: undefined,
orderId: undefined,
flowType: undefined,
money: undefined,
comments: '',
toUserId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
isUpdate.value = false;
}
} else {
@@ -216,3 +350,49 @@
{ immediate: true }
);
</script>
<style lang="less" scoped>
.flow-type-option {
display: flex;
align-items: center;
.ant-tag {
margin-right: 8px;
}
span {
color: #666;
font-size: 12px;
}
}
.amount-preview {
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 600;
font-size: 14px;
}
}
}
: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-select-selection-item) {
display: flex;
align-items: center;
}
:deep(.ant-input-number) {
width: 100%;
}
</style>

View File

@@ -3,7 +3,7 @@
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopDealerCapitalId"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
@@ -100,47 +100,82 @@
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
width: 80,
fixed: 'left'
},
{
title: '分销商用户ID',
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 100,
fixed: 'left'
},
{
title: '订单ID',
dataIndex: 'orderId',
key: 'orderId',
align: 'center',
},
{
title: '资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入)',
title: '流动类型',
dataIndex: 'flowType',
key: 'flowType',
align: 'center',
width: 120,
customRender: ({ text }) => {
const typeMap = {
10: { text: '佣金收入', color: 'success' },
20: { text: '提现支出', color: 'warning' },
30: { text: '转账支出', color: 'error' },
40: { text: '转账收入', color: 'processing' }
};
const type = typeMap[text] || { text: '未知', color: 'default' };
return { type: 'tag', props: { color: type.color }, children: type.text };
}
},
{
title: '金额',
dataIndex: 'money',
key: 'money',
align: 'center',
width: 120,
customRender: ({ text, record }) => {
const amount = parseFloat(text || '0').toFixed(2);
const isIncome = record.flowType === 10 || record.flowType === 40;
return {
type: 'span',
props: {
style: {
color: isIncome ? '#52c41a' : '#ff4d4f',
fontWeight: 'bold'
}
},
children: `${isIncome ? '+' : '-'}¥${amount}`
};
}
},
{
title: '关联订单',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center',
customRender: ({ text }) => text || '-'
},
{
title: '对方用户',
dataIndex: 'toUserId',
key: 'toUserId',
align: 'center',
width: 100,
customRender: ({ text }) => text ? `ID: ${text}` : '-'
},
{
title: '描述',
dataIndex: 'describe',
key: 'describe',
align: 'center',
},
{
title: '对方用户ID',
dataIndex: 'toUserId',
key: 'toUserId',
align: 'center',
align: 'left',
width: 200,
ellipsis: true,
customRender: ({ text }) => text || '-'
},
{
title: '创建时间',
@@ -187,7 +222,7 @@
/* 删除单个 */
const remove = (row: ShopDealerCapital) => {
const hide = message.loading('请求中..', 0);
removeShopDealerCapital(row.shopDealerCapitalId)
removeShopDealerCapital(row.id)
.then((msg) => {
hide();
message.success(msg);
@@ -212,7 +247,7 @@
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerCapital(selection.value.map((d) => d.shopDealerCapitalId))
removeBatchShopDealerCapital(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);

View File

@@ -0,0 +1,83 @@
<!-- 经销商订单导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="导入分销订单"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined/>
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
<div class="ant-upload-text text-gray-400">
<div>1必须按<a href="https://oss.wsdns.cn/20251018/408b805ec3cd4084a4dc686e130af578.xlsx" target="_blank">导入模版</a>的格式上传</div>
<div>2导入成功确认结算完成佣金的发放</div>
</div>
</a-spin>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {message} from 'ant-design-vue/es';
import {CloudUploadOutlined} from '@ant-design/icons-vue';
import {importSdyDealerOrder} from "@/api/sdy/sdyDealerOrder";
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
// 是否打开弹窗
visible: boolean;
}>();
// 导入请求状态
const loading = ref(false);
/* 上传 */
const doUpload = ({file}) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importSdyDealerOrder(file)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

View File

@@ -1,42 +1,184 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
<div class="flex items-center gap-20">
<!-- 搜索表单 -->
<a-form
:model="where"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item>
<a-space>
<a-button
danger
class="ele-btn-icon"
v-if="selection.length > 0"
:disabled="selection?.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined/>
</template>
<span>批量删除</span>
</a-button>
</a-space>
</a-form-item>
<!-- <a-form-item label="订单状态">-->
<!-- <a-select-->
<!-- v-model:value="where.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="where.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-input-search
allow-clear
placeholder="请输入关键词"
style="width: 240px"
v-model:value="where.keywords"
@search="handleSearch"
/>
<!-- <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>
<a-divider type="vertical"/>
<a-space>
<!-- <a-button @click="exportData" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <ExportOutlined />-->
<!-- </template>-->
<!-- 导出数据-->
<!-- </a-button>-->
<a-button @click="openImport" class="ele-btn-icon">
<template #icon>
<UploadOutlined/>
</template>
导入数据
</a-button>
<a-button
type="primary"
danger
@click="batchSettle"
:disabled="selection?.length === 0"
>
<template #icon>
<DollarOutlined/>
</template>
批量结算
</a-button>
</a-space>
</div>
<!-- 导入弹窗 -->
<Import v-model:visible="showImport" @done="emit('importDone')"/>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import {ref} from 'vue';
import {
DollarOutlined,
UploadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import type {ShopDealerOrderParam} from '@/api/shop/shopDealerOrder/model';
import Import from './Import.vue';
import useSearch from "@/utils/use-search";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
withDefaults(
defineProps<{
// 选中的数据
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const emit = defineEmits<{
(e: 'search', where?: ShopDealerOrderParam): void;
(e: 'batchSettle'): void;
(e: 'export'): void;
(e: 'importDone'): void;
(e: 'remove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
// 是否显示导入弹窗
const showImport = ref(false);
watch(
() => props.selection,
() => {}
);
// 搜索表单
const {where, resetFields} = useSearch<ShopDealerOrderParam>({
orderNo: '',
productName: '',
isInvalid: undefined,
isSettled: undefined
});
// 搜索
const handleSearch = () => {
const searchParams = {...where};
// 清除空值
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 : '';
// });
resetFields();
emit('search', {});
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
// 批量结算
const batchSettle = () => {
emit('batchSettle');
};
// 导出数据
const exportData = () => {
emit('export');
};
// 打开导入弹窗
const openImport = () => {
showImport.value = true;
};
</script>

View File

@@ -1,266 +1,352 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销订单记录表' : '添加分销订单记录表'"
:title="isUpdate ? '分销订单' : '分销订单'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:okText="`立即结算`"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="买家用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入买家用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="订单ID" name="orderId">
<a-input
allow-clear
placeholder="请输入订单ID"
v-model:value="form.orderId"
/>
</a-form-item>
<a-form-item label="订单总金额(不含运费)" name="orderPrice">
<a-input
allow-clear
placeholder="请输入订单总金额(不含运费)"
v-model:value="form.orderPrice"
/>
</a-form-item>
<a-form-item label="分销商用户id(一级)" name="firstUserId">
<a-input
allow-clear
placeholder="请输入分销商用户id(一级)"
v-model:value="form.firstUserId"
/>
</a-form-item>
<a-form-item label="分销商用户id(二级)" name="secondUserId">
<a-input
allow-clear
placeholder="请输入分销商用户id(二级)"
v-model:value="form.secondUserId"
/>
</a-form-item>
<a-form-item label="分销商用户id(三级)" name="thirdUserId">
<a-input
allow-clear
placeholder="请输入分销商用户id(三级)"
v-model:value="form.thirdUserId"
/>
</a-form-item>
<a-form-item label="分销佣金(一级)" name="firstMoney">
<a-input
allow-clear
placeholder="请输入分销佣金(一级)"
v-model:value="form.firstMoney"
/>
</a-form-item>
<a-form-item label="分销佣金(二级)" name="secondMoney">
<a-input
allow-clear
placeholder="请输入分销佣金(二级)"
v-model:value="form.secondMoney"
/>
</a-form-item>
<a-form-item label="分销佣金(三级)" name="thirdMoney">
<a-input
allow-clear
placeholder="请输入分销佣金(三级)"
v-model:value="form.thirdMoney"
/>
</a-form-item>
<a-form-item label="订单是否失效(0未失效 1已失效)" name="isInvalid">
<a-input
allow-clear
placeholder="请输入订单是否失效(0未失效 1已失效)"
v-model:value="form.isInvalid"
/>
</a-form-item>
<a-form-item label="佣金结算(0未结算 1已结算)" name="isSettled">
<a-input
allow-clear
placeholder="请输入佣金结算(0未结算 1已结算)"
v-model:value="form.isSettled"
/>
</a-form-item>
<a-form-item label="结算时间" name="settleTime">
<a-input
allow-clear
placeholder="请输入结算时间"
v-model:value="form.settleTime"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
<!-- 订单基本信息 -->
<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="title">
{{ form.title }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="订单编号" name="orderNo">
{{ form.orderNo }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="结算电量" name="orderPrice">
{{ parseFloat(form.orderPrice || 0).toFixed(2) }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="换算成度" name="dealerPrice">
{{ parseFloat(form.degreePrice || 0).toFixed(2) }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="税率" name="rate">
{{ form.rate }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="单价" name="price">
{{ form.price }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="结算金额" name="payPrice">
{{ parseFloat(form.settledPrice || 0).toFixed(2) }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="实发金额" name="payPrice">
{{ parseFloat(form.payPrice || 0).toFixed(2) }}
</a-form-item>
</a-col>
</a-row>
<div class="font-bold text-gray-400 bg-gray-50">开发调试</div>
<div class="text-gray-400 bg-gray-50">
<div>业务员({{ form.userId }}){{ form.nickname }}</div>
<div>一级分销商({{ form.firstUserId }}){{ form.firstNickname }}一级佣金30%{{ form.firstMoney }}</div>
<div>二级分销商({{ form.secondUserId }}){{ form.secondNickname }}二级佣金10%{{ form.secondMoney }}</div>
<div>三级分销商({{ form.thirdUserId }}){{ form.thirdNickname }}三级佣金60%{{ form.thirdMoney }}</div>
</div>
<!-- 分销商信息 -->
<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="orange">一级佣金30%</a-tag>
</h4>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户ID" name="firstUserId">
{{ form.firstUserId }}
</a-form-item>
<a-form-item label="昵称" name="firstNickname">
{{ form.firstNickname }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="占比" name="rate">
{{ '30%' }}
</a-form-item>
<a-form-item label="获取收益" name="firstMoney">
{{ form.firstMoney }}
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 二级分销商 -->
<div class="dealer-section">
<h4 class="dealer-title">
<a-tag color="orange">二级佣金10%</a-tag>
</h4>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户ID" name="secondUserId">
{{ form.secondUserId }}
</a-form-item>
<a-form-item label="昵称" name="nickname">
{{ form.secondNickname }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="占比" name="rate">
10%
</a-form-item>
<a-form-item label="获取收益" name="firstMoney">
{{ form.secondMoney }}
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 三级分销商 -->
<div class="dealer-section" v-if="form.thirdUserId > 0">
<h4 class="dealer-title">
<a-tag color="orange">三级佣金60%</a-tag>
</h4>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户ID" name="thirdUserId">
{{ form.thirdUserId }}
</a-form-item>
<a-form-item label="昵称" name="thirdNickname">
{{ form.thirdNickname }}
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="占比" name="rate">
{{ '60%' }}
</a-form-item>
<a-form-item label="获取收益" name="thirdMoney">
{{ form.thirdMoney }}
</a-form-item>
</a-col>
</a-row>
</div>
<a-form-item label="结算时间" name="settleTime" v-if="form.isSettled === 1">
{{ form.settleTime }}
</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 { 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';
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject} from 'ele-admin-pro';
import {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model';
import {FormInstance} from 'ant-design-vue/es/form';
import {updateSdyDealerOrder} from "@/api/sdy/sdyDealerOrder";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerOrder | null;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerOrder | 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 maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 用户信息
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: undefined,
isSettled: undefined,
settleTime: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopDealerOrderId: undefined,
shopDealerOrderName: '',
status: 0,
comments: '',
sortNumber: 100
});
// 表单数据
const form = reactive<ShopDealerOrder>({
id: undefined,
userId: undefined,
nickname: undefined,
orderNo: undefined,
title: undefined,
orderPrice: undefined,
settledPrice: undefined,
degreePrice: undefined,
price: undefined,
month: undefined,
payPrice: undefined,
firstUserId: undefined,
secondUserId: undefined,
thirdUserId: undefined,
firstMoney: undefined,
secondMoney: undefined,
thirdMoney: undefined,
firstNickname: undefined,
secondNickname: undefined,
thirdNickname: undefined,
rate: undefined,
comments: undefined,
isInvalid: 0,
isSettled: 0,
settleTime: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopDealerOrderName: [
{
required: true,
type: 'string',
message: '请填写分销商订单记录表名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
// 表单验证规则
const rules = reactive({
userId: [
{
required: true,
message: '请选择用户ID',
trigger: 'blur'
}
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) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
if (form.isSettled == 1) {
message.error('请勿重复结算');
return;
}
if (form.userId == 0) {
message.error('未签约');
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
isSettled: 1
};
updateSdyDealerOrder(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
console.log(localStorage.getItem(''))
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
resetFields();
// 重置为默认值
Object.assign(form, {
id: undefined,
userId: undefined,
orderNo: undefined,
orderPrice: undefined,
firstUserId: undefined,
secondUserId: undefined,
thirdUserId: undefined,
firstMoney: undefined,
secondMoney: undefined,
thirdMoney: undefined,
isInvalid: 0,
isSettled: 0,
settleTime: undefined
});
isUpdate.value = false;
}
},
{ immediate: true }
);
} 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

@@ -1,292 +1,433 @@
<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"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@batchSettle="batchSettle"
@export="handleExport"
@remove="removeBatch"
@importDone="reload"
/>
</template>
<template #bodyCell="{ column, record }">
<!-- 编辑弹窗 -->
<ShopDealerOrderEdit v-model:visible="showEdit" :data="current" @done="reload" />
<template v-if="column.key === 'title'">
<div>{{ record.title }}</div>
<div class="text-gray-400">用户ID{{ record.userId }}</div>
</template>
<template v-if="column.key === 'orderPrice'">
{{ record.orderPrice.toFixed(2) }}
</template>
<template v-if="column.key === 'degreePrice'">
{{ record.degreePrice.toFixed(2) }}
</template>
<template v-if="column.key === 'price'">
{{ record.price }}
</template>
<template v-if="column.key === 'settledPrice'">
{{ record.settledPrice.toFixed(2) }}
</template>
<template v-if="column.key === 'payPrice'">
{{ record.payPrice.toFixed(2) }}
</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="orange">未结算</a-tag>
<a-tag v-if="record.isSettled === 1" color="success">已结算</a-tag>
</template>
<template v-if="column.key === 'createTime'">
<div class="flex flex-col">
<a-tooltip title="创建时间">
<span class="text-gray-500">{{ record.createTime }}</span>
</a-tooltip>
<a-tooltip title="结算时间">
<span class="text-purple-500">{{ record.settleTime }}</span>
</a-tooltip>
</div>
</template>
<template v-if="column.key === 'action'">
<template v-if="record.isSettled === 0 && record.isInvalid === 0">
<a @click="openEdit(record)" class="ele-text-success">
结算
</a>
<a-divider type="vertical"/>
</template>
<a-popconfirm
v-if="record.isSettled === 0"
title="确定要删除吗?"
@confirm="remove(record)"
placement="topRight"
>
<a class="text-red-500">
删除
</a>
</a-popconfirm>
</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 } 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';
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} 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';
import {exportSdyDealerOrder} from "@/api/sdy/sdyDealerOrder";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格实例
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 selection = ref<ShopDealerOrder[]>([]);
// 当前编辑数据
const current = ref<ShopDealerOrder | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
// 当前搜索条件
const currentWhere = ref<ShopDealerOrderParam>({});
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
// 保存当前搜索条件用于导出
currentWhere.value = {...where};
// 未结算订单
where.isSettled = 0;
where.myOrder = 1;
return pageShopDealerOrder({
...where,
...orders,
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopDealerOrder({
...where,
...orders,
page,
limit
});
};
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '买家用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '订单ID',
dataIndex: 'orderId',
key: 'orderId',
align: 'center',
},
{
title: '订单总金额(不含运费)',
dataIndex: 'orderPrice',
key: 'orderPrice',
align: 'center',
},
{
title: '分销商用户id(一级)',
dataIndex: 'firstUserId',
key: 'firstUserId',
align: 'center',
},
{
title: '分销商用户id(二级)',
dataIndex: 'secondUserId',
key: 'secondUserId',
align: 'center',
},
{
title: '分销商用户id(三级)',
dataIndex: 'thirdUserId',
key: 'thirdUserId',
align: 'center',
},
{
title: '分销佣金(一级)',
dataIndex: 'firstMoney',
key: 'firstMoney',
align: 'center',
},
{
title: '分销佣金(二级)',
dataIndex: 'secondMoney',
key: 'secondMoney',
align: 'center',
},
{
title: '分销佣金(三级)',
dataIndex: 'thirdMoney',
key: 'thirdMoney',
align: 'center',
},
{
title: '订单是否失效(0未失效 1已失效)',
dataIndex: 'isInvalid',
key: 'isInvalid',
align: 'center',
},
{
title: '佣金结算(0未结算 1已结算)',
dataIndex: 'isSettled',
key: 'isSettled',
align: 'center',
},
{
title: '结算时间',
dataIndex: 'settleTime',
key: 'settleTime',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '订单编号',
dataIndex: 'orderNo',
key: 'orderNo'
},
{
title: '客户名称',
dataIndex: 'title',
key: 'title',
width: 220,
},
{
title: '结算电量',
dataIndex: 'orderPrice',
key: 'orderPrice',
align: 'center'
},
{
title: '换算成度',
dataIndex: 'degreePrice',
key: 'degreePrice',
align: 'center'
},
{
title: '结算单价',
dataIndex: 'price',
key: 'price',
align: 'center'
},
{
title: '结算金额',
dataIndex: 'settledPrice',
key: 'settledPrice',
align: 'center'
},
{
title: '税费',
dataIndex: 'rate',
key: 'rate',
align: 'center'
},
{
title: '实发金额',
dataIndex: 'payPrice',
key: 'payPrice',
align: 'center'
},
{
title: '签约状态',
dataIndex: 'isInvalid',
key: 'isInvalid',
align: 'center',
width: 100
},
{
title: '月份',
dataIndex: 'month',
key: 'month',
align: 'center',
width: 100
},
{
title: '结算状态',
dataIndex: 'isSettled',
key: 'isSettled',
align: 'center',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center'
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopDealerOrderParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 搜索 */
const reload = (where?: ShopDealerOrderParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerOrder) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 批量结算 */
const batchSettle = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
const validOrders = selection.value.filter(order =>
order.isSettled === 0 && order.isInvalid === 0
);
/* 删除单个 */
const remove = (row: ShopDealerOrder) => {
const hide = message.loading('请求中..', 0);
removeShopDealerOrder(row.shopDealerOrderId)
.then((msg) => {
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(msg);
message.success(`成功结算 ${validOrders.length} 个订单`);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}, 1500);
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerOrder(selection.value.map((d) => d.shopDealerOrderId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 导出数据 */
const handleExport = () => {
// 调用导出API传入当前搜索条件
exportSdyDealerOrder(currentWhere.value);
};
/* 打开编辑弹窗 */
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 query = () => {
loading.value = true;
};
/* 批量删除 */
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 customRow = (record: ShopDealerOrder) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerOrder) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
query();
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerOrder'
};
export default {
name: 'ShopDealerOrder'
};
</script>
<style lang="less" scoped></style>
<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

@@ -0,0 +1,751 @@
<template>
<a-page-header title="分销海报设置" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '24px' }">
<div class="poster-container">
<!-- 说明信息 -->
<a-alert
message="分销商海报设置说明"
type="info"
show-icon
class="poster-alert"
>
<template #description>
<div>
<p>1. 可根据需要二维码昵称等动态位置</p>
<p>2. 保存后将海报生成新图片这里一般不变更清除图片缓存</p>
</div>
</template>
</a-alert>
<div class="poster-content">
<!-- 左侧海报预览 -->
<div class="poster-preview">
<div class="poster-canvas" ref="posterCanvasRef">
<img
:src="posterConfig.backgroundImage || defaultBackground"
alt="海报背景"
class="poster-bg"
@load="onBackgroundLoad"
/>
<!-- 动态元素层 -->
<div class="poster-elements">
<!-- 头像 -->
<div
class="poster-avatar draggable-element"
:style="getElementStyle('avatar')"
v-if="posterConfig.showAvatar"
@mousedown="startDrag($event, 'avatar')"
>
<img :src="posterConfig.avatarUrl || defaultAvatar" alt="头像" />
<div class="element-handle">拖拽</div>
</div>
<!-- 昵称 -->
<div
class="poster-nickname draggable-element"
:style="getElementStyle('nickname')"
v-if="posterConfig.showNickname"
@mousedown="startDrag($event, 'nickname')"
>
{{ posterConfig.nickname || '这里是昵称' }}
<div class="element-handle">拖拽</div>
</div>
<!-- 二维码 -->
<div
class="poster-qrcode draggable-element"
:style="getElementStyle('qrcode')"
v-if="posterConfig.showQrcode"
@mousedown="startDrag($event, 'qrcode')"
>
<img :src="posterConfig.qrcodeUrl || defaultQrcode" alt="二维码" />
<div class="element-handle">拖拽</div>
</div>
</div>
</div>
</div>
<!-- 右侧设置面板 -->
<div class="poster-settings">
<!-- 预设模板 -->
<div class="setting-section">
<h4>预设模板</h4>
<div class="template-grid">
<div
v-for="template in templates"
:key="template.id"
class="template-item"
:class="{ active: currentTemplate === template.id }"
@click="applyTemplate(template)"
>
<img :src="template.preview" :alt="template.name" />
<span>{{ template.name }}</span>
</div>
</div>
</div>
<!-- 背景图片设置 -->
<div class="setting-section">
<h4>背景图片</h4>
<div class="background-preview">
<img :src="posterConfig.backgroundImage || defaultBackground" alt="背景预览" />
</div>
<a-upload
:file-list="backgroundFileList"
list-type="picture"
:max-count="1"
@change="handleBackgroundChange"
:before-upload="beforeUpload"
>
<a-button>
<UploadOutlined /> 选择背景图片
</a-button>
</a-upload>
<div class="setting-desc">
图片尺寸宽750像素 高1200<br>
请务必按尺寸上传否则生成的海报会变形
</div>
</div>
<!-- 头像设置 -->
<div class="setting-section">
<h4>头像设置</h4>
<div class="setting-row">
<span>头像宽度</span>
<a-input-number
v-model:value="posterConfig.avatarWidth"
:min="20"
:max="200"
@change="updatePreview"
/>
</div>
<div class="setting-row">
<span>头像样式</span>
<a-radio-group v-model:value="posterConfig.avatarShape" @change="updatePreview">
<a-radio value="circle">圆形</a-radio>
<a-radio value="square">方形</a-radio>
</a-radio-group>
</div>
</div>
<!-- 昵称设置 -->
<div class="setting-section">
<h4>昵称字体大小</h4>
<a-input-number
v-model:value="posterConfig.nicknameFontSize"
:min="12"
:max="48"
@change="updatePreview"
/>
</div>
<!-- 昵称颜色设置 -->
<div class="setting-section">
<h4>昵称字体颜色</h4>
<div class="color-picker">
<input
type="color"
v-model="posterConfig.nicknameColor"
@change="updatePreview"
/>
<span>{{ posterConfig.nicknameColor }}</span>
</div>
</div>
<!-- 二维码设置 -->
<div class="setting-section">
<h4>二维码宽度</h4>
<a-input-number
v-model:value="posterConfig.qrcodeWidth"
:min="50"
:max="200"
@change="updatePreview"
/>
</div>
<!-- 保存按钮 -->
<div class="setting-footer">
<a-button type="primary" size="large" @click="savePosterConfigData" :loading="saving">
保存
</a-button>
</div>
</div>
</div>
</div>
</a-card>
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import {
getCurrentPosterConfig,
savePosterConfig,
uploadPosterBackground
} from '@/api/shop/shopDealerPoster';
import type { PosterConfig } from '@/api/shop/shopDealerPoster/model';
// 海报配置
const posterConfig = reactive({
backgroundImage: 'https://pro2.yiovo.com/assets/store/img/dealer/backdrop.png',
showAvatar: true,
avatarUrl: 'https://pro2.yiovo.com/assets/store/img/dealer/avatar.png',
avatarWidth: 50,
avatarShape: 'circle',
showNickname: true,
nickname: '这里是昵称',
nicknameFontSize: 12,
nicknameColor: '#000000',
showQrcode: true,
qrcodeUrl: '',
qrcodeWidth: 66,
// 元素位置配置
elements: {
avatar: { x: 50, y: 50 },
nickname: { x: 120, y: 65 },
qrcode: { x: 300, y: 500 }
}
});
// 默认资源 - 使用在线图片作为占位符
const defaultBackground = 'https://via.placeholder.com/750x1200/ff6b35/ffffff?text=分享赚钱';
const defaultAvatar = 'https://via.placeholder.com/100x100/4CAF50/ffffff?text=头像';
const defaultQrcode = 'https://via.placeholder.com/100x100/2196F3/ffffff?text=二维码';
// 状态
const saving = ref(false);
const backgroundFileList = ref([]);
const posterCanvasRef = ref();
// 拖拽状态
const dragging = ref(false);
const dragElement = ref('');
const dragStart = ref({ x: 0, y: 0 });
const elementStart = ref({ x: 0, y: 0 });
// 当前模板
const currentTemplate = ref(1);
// 预设模板
const templates = ref([
{
id: 1,
name: '经典模板',
preview: 'https://via.placeholder.com/120x180/ff6b35/ffffff?text=经典',
config: {
backgroundImage: 'https://via.placeholder.com/750x1200/ff6b35/ffffff?text=分享赚钱',
elements: {
avatar: { x: 50, y: 50 },
nickname: { x: 120, y: 65 },
qrcode: { x: 300, y: 500 }
},
avatarWidth: 50,
nicknameFontSize: 16,
nicknameColor: '#ffffff',
qrcodeWidth: 100
}
},
{
id: 2,
name: '简约模板',
preview: 'https://via.placeholder.com/120x180/2196F3/ffffff?text=简约',
config: {
backgroundImage: 'https://via.placeholder.com/750x1200/2196F3/ffffff?text=简约风格',
elements: {
avatar: { x: 325, y: 100 },
nickname: { x: 300, y: 200 },
qrcode: { x: 325, y: 800 }
},
avatarWidth: 80,
nicknameFontSize: 20,
nicknameColor: '#ffffff',
qrcodeWidth: 120
}
},
{
id: 3,
name: '活力模板',
preview: 'https://via.placeholder.com/120x180/4CAF50/ffffff?text=活力',
config: {
backgroundImage: 'https://via.placeholder.com/750x1200/4CAF50/ffffff?text=活力四射',
elements: {
avatar: { x: 100, y: 300 },
nickname: { x: 200, y: 320 },
qrcode: { x: 500, y: 300 }
},
avatarWidth: 60,
nicknameFontSize: 18,
nicknameColor: '#ffffff',
qrcodeWidth: 80
}
}
]);
/* 获取元素样式 */
const getElementStyle = (elementType: string) => {
const element = posterConfig.elements[elementType];
const baseStyle = {
position: 'absolute',
left: `${element.x}px`,
top: `${element.y}px`
};
switch (elementType) {
case 'avatar':
return {
...baseStyle,
width: `${posterConfig.avatarWidth}px`,
height: `${posterConfig.avatarWidth}px`,
borderRadius: posterConfig.avatarShape === 'circle' ? '50%' : '4px',
overflow: 'hidden'
};
case 'nickname':
return {
...baseStyle,
fontSize: `${posterConfig.nicknameFontSize}px`,
color: posterConfig.nicknameColor,
fontWeight: 'bold'
};
case 'qrcode':
return {
...baseStyle,
width: `${posterConfig.qrcodeWidth}px`,
height: `${posterConfig.qrcodeWidth}px`
};
default:
return baseStyle;
}
};
/* 背景图片加载完成 */
const onBackgroundLoad = () => {
updatePreview();
};
/* 更新预览 */
const updatePreview = () => {
// 这里可以添加实时预览更新逻辑
console.log('更新预览');
};
/* 应用模板 */
const applyTemplate = (template: any) => {
currentTemplate.value = template.id;
// 应用模板配置
posterConfig.backgroundImage = template.config.backgroundImage;
posterConfig.elements = { ...template.config.elements };
posterConfig.avatarWidth = template.config.avatarWidth;
posterConfig.nicknameFontSize = template.config.nicknameFontSize;
posterConfig.nicknameColor = template.config.nicknameColor;
posterConfig.qrcodeWidth = template.config.qrcodeWidth;
updatePreview();
message.success(`已应用${template.name}`);
};
/* 开始拖拽 */
const startDrag = (event: MouseEvent, elementType: string) => {
event.preventDefault();
dragging.value = true;
dragElement.value = elementType;
dragStart.value = {
x: event.clientX,
y: event.clientY
};
const element = posterConfig.elements[elementType];
elementStart.value = {
x: element.x,
y: element.y
};
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
};
/* 拖拽中 */
const onDrag = (event: MouseEvent) => {
if (!dragging.value || !dragElement.value) return;
const deltaX = event.clientX - dragStart.value.x;
const deltaY = event.clientY - dragStart.value.y;
// 计算新位置,考虑缩放比例
const scale = 0.4; // 预览区域的缩放比例
const newX = Math.max(0, Math.min(750 - 100, elementStart.value.x + deltaX / scale));
const newY = Math.max(0, Math.min(1200 - 100, elementStart.value.y + deltaY / scale));
posterConfig.elements[dragElement.value] = {
x: Math.round(newX),
y: Math.round(newY)
};
};
/* 停止拖拽 */
const stopDrag = () => {
dragging.value = false;
dragElement.value = '';
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
};
/* 背景图片上传前检查 */
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片大小不能超过 5MB!');
return false;
}
return true;
};
/* 背景图片变更 */
const handleBackgroundChange = async (info: any) => {
backgroundFileList.value = info.fileList;
if (info.file.status === 'uploading') {
return;
}
if (info.file.originFileObj) {
try {
const result = await uploadPosterBackground(info.file.originFileObj);
posterConfig.backgroundImage = result.url;
message.success('背景图片上传成功');
updatePreview();
} catch (error) {
console.error('上传失败:', error);
message.error('背景图片上传失败');
}
}
};
/* 保存海报配置 */
const savePosterConfigData = async () => {
saving.value = true;
try {
const configData: PosterConfig = {
backgroundImage: posterConfig.backgroundImage,
width: 750,
height: 1200,
showAvatar: posterConfig.showAvatar,
avatarUrl: posterConfig.avatarUrl,
avatarWidth: posterConfig.avatarWidth,
avatarShape: posterConfig.avatarShape,
showNickname: posterConfig.showNickname,
nickname: posterConfig.nickname,
nicknameFontSize: posterConfig.nicknameFontSize,
nicknameColor: posterConfig.nicknameColor,
showQrcode: posterConfig.showQrcode,
qrcodeUrl: posterConfig.qrcodeUrl,
qrcodeWidth: posterConfig.qrcodeWidth,
elements: posterConfig.elements
};
await savePosterConfig(configData);
message.success('海报配置保存成功');
} catch (error) {
console.error('保存失败:', error);
message.error('保存失败');
} finally {
saving.value = false;
}
};
/* 加载海报配置 */
const loadPosterConfig = async () => {
try {
const config = await getCurrentPosterConfig();
if (config) {
Object.assign(posterConfig, config);
updatePreview();
}
} catch (error) {
console.error('加载配置失败:', error);
// 使用默认配置,不显示错误信息
}
};
onMounted(() => {
loadPosterConfig();
});
</script>
<script lang="ts">
export default {
name: 'ShopDealerPoster'
};
</script>
<style lang="less" scoped>
.poster-container {
max-width: 1400px;
margin: 0 auto;
}
.poster-alert {
margin-bottom: 24px;
:deep(.ant-alert-description) {
p {
margin: 4px 0;
color: #666;
}
}
}
.poster-content {
display: flex;
gap: 32px;
align-items: flex-start;
}
.poster-preview {
flex: 0 0 400px;
.poster-canvas {
position: relative;
width: 300px;
height: 480px;
border: 2px solid #d9d9d9;
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
margin: 0 auto;
.poster-bg {
width: 100%;
height: 100%;
object-fit: cover;
}
.poster-elements {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
.draggable-element {
pointer-events: auto;
cursor: move;
border: 2px dashed transparent;
transition: border-color 0.2s;
&:hover {
border-color: #1890ff;
.element-handle {
opacity: 1;
}
}
.element-handle {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background: #1890ff;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
white-space: nowrap;
}
}
.poster-avatar {
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.poster-nickname {
white-space: nowrap;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
padding: 4px 8px;
background: rgba(255,255,255,0.8);
border-radius: 4px;
}
.poster-qrcode {
background: white;
border-radius: 4px;
padding: 4px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
}
}
.poster-settings {
flex: 1;
.setting-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
h4 {
margin: 0 0 12px 0;
color: #333;
font-weight: 600;
}
.background-preview {
width: 120px;
height: 80px;
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
margin-bottom: 12px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.setting-desc {
color: #999;
font-size: 12px;
margin-top: 8px;
line-height: 1.4;
}
.template-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
.template-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
border: 2px solid #d9d9d9;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #1890ff;
transform: translateY(-2px);
}
&.active {
border-color: #1890ff;
background: #f0f8ff;
}
img {
width: 60px;
height: 90px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 6px;
}
span {
font-size: 12px;
color: #666;
text-align: center;
}
}
}
.setting-row {
display: flex;
align-items: center;
margin-bottom: 12px;
span {
width: 100px;
color: #666;
}
}
.color-picker {
display: flex;
align-items: center;
gap: 12px;
input[type="color"] {
width: 40px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
}
span {
color: #666;
font-family: monospace;
}
}
}
.setting-footer {
text-align: center;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
}
:deep(.ant-upload-list) {
margin-top: 8px;
}
:deep(.ant-input-number) {
width: 120px;
}
:deep(.ant-radio-group) {
.ant-radio-wrapper {
margin-right: 16px;
}
}
// 响应式设计
@media (max-width: 1200px) {
.poster-content {
flex-direction: column;
align-items: center;
}
.poster-preview {
flex: none;
margin-bottom: 24px;
}
.poster-settings {
width: 100%;
max-width: 600px;
}
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<a-modal
:visible="visible"
:title="title"
:width="1000"
:footer="null"
@cancel="handleCancel"
>
<div class="tree-container">
<v-chart
ref="chartRef"
class="chart"
:option="chartOption"
:loading="loading"
autoresize
/>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
import { use } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { TreeChart } from 'echarts/charts';
import {
TooltipComponent,
TitleComponent
} from 'echarts/components';
import VChart from 'vue-echarts';
import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model';
// 注册 echarts 组件
use([
CanvasRenderer,
TreeChart,
TooltipComponent,
TitleComponent
]);
// 定义组件属性
const props = withDefaults(defineProps<{
visible: boolean;
data?: ShopDealerReferee[];
title?: string;
}>(), {
visible: false,
data: () => [],
title: '推荐关系树'
});
// 定义事件
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'cancel'): void;
}>();
// 图表引用
const chartRef = ref<InstanceType<typeof VChart> | null>(null);
// 加载状态
const loading = ref(false);
// 处理取消事件
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
};
// 转换数据为树形结构
const transformToTreeData = (data: ShopDealerReferee[]) => {
if (!data || data.length === 0) {
return { name: '暂无数据', children: [] };
}
// 构建节点映射
const nodeMap = new Map<number, any>();
const rootNodes: any[] = [];
// 创建所有节点
data.forEach(item => {
// 推荐人节点
if (item.dealerId && !nodeMap.has(item.dealerId)) {
nodeMap.set(item.dealerId, {
id: item.dealerId,
name: `推荐人\nID:${item.dealerId}\n${item.dealerName || ''}`,
level: 'dealer',
children: []
});
}
// 被推荐人节点
if (item.userId && !nodeMap.has(item.userId)) {
nodeMap.set(item.userId, {
id: item.userId,
name: `被推荐人\nID:${item.userId}\n${item.nickname || ''}`,
level: 'user',
children: []
});
}
});
// 构建关系树
data.forEach(item => {
if (!item.dealerId || !item.userId) return;
const dealerNode = nodeMap.get(item.dealerId);
const userNode = nodeMap.get(item.userId);
if (dealerNode && userNode) {
// 添加层级标签
const levelText = { 1: '一级', 2: '二级', 3: '三级' }[item.level || 0] || `${item.level || 0}`;
userNode.name += `\n${levelText}推荐`;
dealerNode.children.push(userNode);
}
});
// 查找根节点(没有被推荐关系的节点)
const referencedIds = new Set(data.map(item => item.userId).filter(id => id));
data.forEach(item => {
if (item.dealerId && !referencedIds.has(item.dealerId)) {
const rootNode = nodeMap.get(item.dealerId);
if (rootNode) {
rootNodes.push(rootNode);
}
}
});
// 如果没有明确的根节点,使用第一个节点作为根
if (rootNodes.length === 0 && data.length > 0 && data[0].dealerId) {
const firstNode = nodeMap.get(data[0].dealerId);
if (firstNode) {
rootNodes.push(firstNode);
}
}
// 如果还是没有根节点,返回默认节点
if (rootNodes.length === 0) {
return { name: '暂无数据', children: [] };
}
return rootNodes[0];
};
// 图表配置
const chartOption = computed(() => {
const treeData = transformToTreeData(props.data || []);
return {
tooltip: {
trigger: 'item',
triggerOn: 'mousemove'
},
series: [
{
type: 'tree',
data: [treeData],
top: '1%',
left: '7%',
bottom: '1%',
right: '20%',
symbolSize: 12,
symbol: 'circle',
orient: 'LR', // 从左到右
expandAndCollapse: true,
label: {
position: 'left',
verticalAlign: 'middle',
align: 'right',
fontSize: 12,
backgroundColor: '#fff',
padding: [2, 4],
borderRadius: 4,
borderWidth: 1,
borderColor: '#ccc'
},
leaves: {
label: {
position: 'right',
verticalAlign: 'middle',
align: 'left'
}
},
emphasis: {
focus: 'descendant'
},
animationDuration: 500,
animationDurationUpdate: 750
}
]
};
});
</script>
<style lang="less" scoped>
.tree-container {
height: 600px;
width: 100%;
}
.chart {
height: 100%;
width: 100%;
}
</style>

View File

@@ -1,42 +1,183 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
<!-- 搜索表单 -->
<a-form
:model="searchForm"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="推荐人ID">
<a-input-number
v-model:value="searchForm.dealerId"
placeholder="请输入推荐人ID"
:min="1"
style="width: 160px"
/>
</a-form-item>
<a-form-item label="被推荐人ID">
<a-input-number
v-model:value="searchForm.userId"
placeholder="请输入被推荐人ID"
:min="1"
style="width: 160px"
/>
</a-form-item>
<a-form-item label="建立时间">
<a-range-picker
v-model:value="searchForm.dateRange"
style="width: 240px"
/>
</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-button @click="exportData" class="ele-btn-icon">
<template #icon>
<ExportOutlined/>
</template>
导出
</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 操作按钮 -->
<!-- <div class="action-buttons">-->
<!-- <a-space>-->
<!-- <a-button type="primary" @click="add" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <PlusOutlined/>-->
<!-- </template>-->
<!-- 建立推荐关系-->
<!-- </a-button>-->
<!-- <a-button @click="viewTree" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <ApartmentOutlined/>-->
<!-- </template>-->
<!-- 推荐关系树-->
<!-- </a-button>-->
<!-- <a-button @click="exportData" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <ExportOutlined/>-->
<!-- </template>-->
<!-- 导出数据-->
<!-- </a-button>-->
<!-- </a-space>-->
<!-- </div>-->
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import {reactive} from 'vue';
import {
PlusOutlined,
SearchOutlined,
ApartmentOutlined,
ExportOutlined
} from '@ant-design/icons-vue';
import type {ShopDealerRefereeParam} from '@/api/shop/shopDealerReferee/model';
import dayjs from 'dayjs';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const props = withDefaults(
defineProps<{
// 选中的数据
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const emit = defineEmits<{
(e: 'search', where?: ShopDealerRefereeParam): void;
(e: 'add'): void;
(e: 'viewTree'): void;
(e: 'export'): void;
}>();
// 新增
const add = () => {
emit('add');
};
// 搜索表单
const searchForm = reactive<any>({
dealerId: undefined,
userId: undefined,
level: undefined,
dateRange: undefined
});
watch(
() => props.selection,
() => {}
);
// 搜索
const handleSearch = () => {
const searchParams: ShopDealerRefereeParam = {};
if (searchForm.dealerId) {
searchParams.dealerId = searchForm.dealerId;
}
if (searchForm.userId) {
searchParams.userId = searchForm.userId;
}
if (searchForm.level) {
searchParams.level = searchForm.level;
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
searchParams.startTime = dayjs(searchForm.dateRange[0]).format('YYYY-MM-DD');
searchParams.endTime = dayjs(searchForm.dateRange[1]).format('YYYY-MM-DD');
}
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
searchForm.dealerId = undefined;
searchForm.userId = undefined;
searchForm.level = undefined;
searchForm.dateRange = undefined;
emit('search', {});
};
// 新增
const add = () => {
emit('add');
};
// 查看推荐树
const viewTree = () => {
emit('viewTree');
};
// 导出数据
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

@@ -5,7 +5,7 @@
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销商推荐关系表' : '添加分销商推荐关系'"
:title="isUpdate ? '编辑推荐' : '添加推荐关系'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -19,34 +19,20 @@
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="分销商用户ID" name="dealerId">
<a-form-item label="推荐人信息" name="dealerId">
<a-input
allow-clear
placeholder="请输入分销商用户ID"
v-model:value="form.dealerId"
/>
</a-form-item>
<a-form-item label="用户id(被推荐人)" name="userId">
<a-form-item label="被推荐人信息" name="userId">
<a-input
allow-clear
placeholder="请输入用户id(被推荐人)"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="推荐关系层级(1,2,3)" name="level">
<a-input
allow-clear
placeholder="请输入推荐关系层级(1,2,3)"
v-model:value="form.level"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
@@ -54,14 +40,13 @@
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { assignObject } from 'ele-admin-pro';
import { addShopDealerReferee, updateShopDealerReferee } from '@/api/shop/shopDealerReferee';
import { ShopDealerReferee } from '@/api/shop/shopDealerReferee/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);
@@ -98,12 +83,7 @@
level: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopDealerRefereeId: undefined,
shopDealerRefereeName: '',
status: 0,
comments: '',
sortNumber: 100
updateTime: undefined
});
/* 更新visible */
@@ -123,20 +103,6 @@
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
@@ -174,13 +140,6 @@
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;

View File

@@ -3,7 +3,7 @@
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopDealerRefereeId"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
@@ -14,29 +14,69 @@
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@viewTree="viewRefereeTree"
@export="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
<template v-if="column.key === 'dealerInfo'">
<div class="user-info">
<div class="user-id">{{ record.dealerName }}({{ record.dealerId }})</div>
<div class="user-id"></div>
<div class="user-role">
<a-tag color="blue">推荐人</a-tag>
</div>
</div>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
<template v-if="column.key === 'relationship'">
<ArrowRightOutlined />
</template>
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<div class="user-id">{{ record.nickname }}({{ record.userId }})</div>
<div class="user-role">
<a-tag color="green">被推荐人</a-tag>
</div>
</div>
</template>
<template v-if="column.key === 'level'">
<a-tag
:color="getLevelColor(record.level || 0)"
class="level-tag"
>
{{ getLevelText(record.level || 0) }}
</a-tag>
</template>
<template v-if="column.key === 'relationChain'">
<a-button
type="link"
size="small"
@click="viewRelationChain(record)"
class="chain-btn"
>
<TeamOutlined /> 查看关系链
</a-button>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
<a @click="openEdit(record)" class="ele-text-primary">
<EditOutlined /> 编辑
</a>
<!-- <a-divider type="vertical" />-->
<!-- <a-popconfirm-->
<!-- title="确定要解除此推荐关系吗?"-->
<!-- @confirm="remove(record)"-->
<!-- placement="topRight"-->
<!-- >-->
<!-- <a class="ele-text-danger">-->
<!-- <DisconnectOutlined /> 解除-->
<!-- </a>-->
<!-- </a-popconfirm>-->
</a-space>
</template>
</template>
@@ -45,13 +85,26 @@
<!-- 编辑弹窗 -->
<ShopDealerRefereeEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 树状图弹窗 -->
<RefereeTree
v-model:visible="showTree"
:data="treeData"
:title="'推荐关系树'"
@cancel="showTree = false"
/>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import {
ExclamationCircleOutlined,
TeamOutlined,
EditOutlined,
ArrowRightOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -61,7 +114,14 @@
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import ShopDealerRefereeEdit from './components/shopDealerRefereeEdit.vue';
import { pageShopDealerReferee, removeShopDealerReferee, removeBatchShopDealerReferee } from '@/api/shop/shopDealerReferee';
import RefereeTree from './components/RefereeTree.vue';
import { utils, writeFile } from 'xlsx';
import {
pageShopDealerReferee,
removeShopDealerReferee,
removeBatchShopDealerReferee,
listShopDealerReferee
} from '@/api/shop/shopDealerReferee';
import type { ShopDealerReferee, ShopDealerRefereeParam } from '@/api/shop/shopDealerReferee/model';
// 表格实例
@@ -73,8 +133,10 @@
const current = ref<ShopDealerReferee | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 是否显示树状图弹窗
const showTree = ref(false);
// 树状图数据
const treeData = ref<ShopDealerReferee[]>([]);
// 加载状态
const loading = ref(true);
@@ -100,55 +162,183 @@
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
title: '推荐人信息',
key: 'dealerInfo',
align: 'left',
width: 150
},
{
title: '分销商用户ID',
dataIndex: 'dealerId',
key: 'dealerId',
title: '',
key: 'relationship',
align: 'center',
width: 50
},
{
title: '用户id(被推荐人)',
dataIndex: 'userId',
key: 'userId',
align: 'center',
title: '被推荐人信息',
key: 'userInfo',
align: 'left',
width: 150
},
// {
// title: '推荐层级',
// key: 'level',
// align: 'center',
// width: 120,
// filters: [
// { text: '一级推荐', value: 1 },
// { text: '二级推荐', value: 2 },
// { text: '三级推荐', value: 3 }
// ]
// },
{
title: '推荐关系层级(1,2,3)',
dataIndex: 'level',
key: 'level',
align: 'center',
},
{
title: '创建时间',
title: '建立时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 120,
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm')
},
{
title: '操作',
key: 'action',
width: 180,
width: 200,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 获取层级颜色 */
const getLevelColor = (level: number) => {
const colors = {
1: 'red',
2: 'orange',
3: 'gold'
};
return colors[level] || 'default';
};
/* 获取层级文本 */
const getLevelText = (level: number) => {
const texts = {
1: '一级推荐',
2: '二级推荐',
3: '三级推荐'
};
return texts[level] || `${level}级推荐`;
};
/* 查看关系链 */
const viewRelationChain = (record: ShopDealerReferee) => {
// 这里可以调用API获取完整的推荐关系链
Modal.info({
title: '推荐关系链',
width: 800,
content: createVNode('div', { class: 'relation-chain' }, [
createVNode('div', { class: 'chain-item' }, [
createVNode('div', { class: 'chain-node dealer' }, [
createVNode('div', { class: 'node-title' }, '推荐人'),
createVNode('div', { class: 'node-id' }, `用户ID: ${record.dealerId}`),
createVNode('div', { class: 'node-level' }, '分销商')
]),
createVNode('div', { class: 'chain-arrow' }, '→'),
createVNode('div', { class: 'chain-node user' }, [
createVNode('div', { class: 'node-title' }, '被推荐人'),
createVNode('div', { class: 'node-id' }, `用户ID: ${record.userId}`),
createVNode('div', { class: 'node-level' }, getLevelText(record.level || 0))
])
]),
createVNode('div', { class: 'chain-info' }, [
createVNode('p', null, `推荐关系建立于: ${toDateString(record.createTime, 'yyyy-MM-dd HH:mm:ss')}`),
createVNode('p', null, '点击可查看更多上下级关系')
])
]),
okText: '关闭'
});
};
/* 查看推荐树 */
const viewRefereeTree = () => {
// 加载所有数据用于树状图展示
loading.value = true;
pageShopDealerReferee({ page: 1, limit: 10000 }) // 获取所有数据
.then((result) => {
treeData.value = result?.list || [];
showTree.value = true;
loading.value = false;
})
.catch((e) => {
loading.value = false;
message.error('加载数据失败: ' + e.message);
});
};
/* 导出数据 */
const exportData = async () => {
try {
// 定义表头
const array: (string | number)[][] = [
[
'推荐人',
'推荐人ID',
'推荐人电话',
'被推荐人',
'被推荐人ID',
'被推荐人电话',
'创建时间'
]
];
// 获取用户列表数据
const list = await listShopDealerReferee({});
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
return;
}
// 将数据转换为Excel行
list.forEach((user: ShopDealerReferee) => {
array.push([
`${user.dealerName}`,
`${user.dealerId}`,
`${user.dealerPhone}`,
`${user.nickname}`,
`${user.userId}`,
`${user.phone}`,
`${user.createTime}`
]);
});
// 生成Excel文件
const sheetName = `shop_dealer_referee`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 }, // 用户ID
{ wch: 15 }, // 账号
];
message.loading('正在生成Excel文件...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
message.destroy();
message.success(`成功导出 ${list.length} 条记录`);
}, 1000);
} catch (error: any) {
message.error(error.message || '导出失败');
}
};
/* 搜索 */
const reload = (where?: ShopDealerRefereeParam) => {
selection.value = [];
@@ -161,15 +351,10 @@
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerReferee) => {
const hide = message.loading('请求中..', 0);
removeShopDealerReferee(row.shopDealerRefereeId)
removeShopDealerReferee(row.id)
.then((msg) => {
hide();
message.success(msg);
@@ -194,7 +379,7 @@
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerReferee(selection.value.map((d) => d.shopDealerRefereeId))
removeBatchShopDealerReferee(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
@@ -235,4 +420,118 @@
};
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.user-info {
.user-id {
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.user-role {
font-size: 12px;
}
}
.level-tag {
font-weight: 600;
font-size: 12px;
}
.chain-btn {
padding: 0;
height: auto;
font-size: 12px;
}
:deep(.referee-detail) {
.detail-section {
h4 {
color: #1890ff;
margin-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 8px;
}
p {
margin: 8px 0;
line-height: 1.6;
}
.level-badge {
font-size: 14px;
}
}
}
:deep(.relation-chain) {
.chain-item {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
}
.chain-node {
padding: 16px;
border-radius: 8px;
text-align: center;
min-width: 120px;
&.dealer {
background: #e6f7ff;
border: 2px solid #1890ff;
}
&.user {
background: #f6ffed;
border: 2px solid #52c41a;
}
.node-title {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.node-id {
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.node-level {
font-size: 12px;
color: #999;
}
}
.chain-arrow {
font-size: 24px;
color: #1890ff;
margin: 0 20px;
font-weight: bold;
}
.chain-info {
background: #fafafa;
padding: 12px;
border-radius: 6px;
margin-top: 16px;
p {
margin: 4px 0;
color: #666;
font-size: 12px;
}
}
}
:deep(.ant-table-tbody > tr > td) {
vertical-align: top;
}
:deep(.ant-tag) {
margin: 2px 4px 2px 0;
}
</style>

View File

@@ -1,11 +1,11 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销商设置' : '添加分销商设置'"
:title="isUpdate ? '编辑分销商设置' : '新增分销商设置'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -14,31 +14,282 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-form-item label="设置项描述" name="describe">
<a-input
allow-clear
placeholder="请输入设置项描述"
v-model:value="form.describe"
/>
</a-form-item>
<a-form-item label="设置内容(json格式)" name="values">
<a-input
allow-clear
placeholder="请输入设置内容(json格式)"
v-model:value="form.values"
/>
</a-form-item>
<a-form-item label="更新时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入更新时间"
v-model:value="form.updateTime"
<!-- 基本信息 -->
<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="key">
<a-select
v-model:value="form.key"
placeholder="请选择设置标识"
@change="onSettingKeyChange"
>
<a-select-option value="commission_rate">
<div class="setting-option">
<a-tag color="blue">佣金比例</a-tag>
<span>分销佣金比例设置</span>
</div>
</a-select-option>
<a-select-option value="withdraw_config">
<div class="setting-option">
<a-tag color="green">提现配置</a-tag>
<span>提现相关参数设置</span>
</div>
</a-select-option>
<a-select-option value="level_config">
<div class="setting-option">
<a-tag color="orange">等级配置</a-tag>
<span>分销商等级设置</span>
</div>
</a-select-option>
<a-select-option value="reward_config">
<div class="setting-option">
<a-tag color="purple">奖励配置</a-tag>
<span>推广奖励设置</span>
</div>
</a-select-option>
<a-select-option value="other">
<div class="setting-option">
<a-tag color="default">其他设置</a-tag>
<span>自定义设置项</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="设置描述" name="describe">
<a-input
placeholder="请输入设置项描述"
v-model:value="form.describe"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 设置内容 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">设置内容</span>
</a-divider>
<!-- 预设配置模板 -->
<div v-if="form.key && form.key !== 'other'" class="config-template">
<a-alert
:message="getTemplateTitle()"
:description="getTemplateDescription()"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<!-- 佣金比例配置 -->
<div v-if="form.key === 'commission_rate'" class="commission-config">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="一级佣金比例">
<a-input-number
v-model:value="configData.firstRate"
:min="0"
:max="100"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter>%</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="二级佣金比例">
<a-input-number
v-model:value="configData.secondRate"
:min="0"
:max="100"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter>%</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="三级佣金比例">
<a-input-number
v-model:value="configData.thirdRate"
:min="0"
:max="100"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter>%</template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 提现配置 -->
<div v-if="form.key === 'withdraw_config'" class="withdraw-config">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="最小提现金额">
<a-input-number
v-model:value="configData.minAmount"
:min="0"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="手续费比例">
<a-input-number
v-model:value="configData.feeRate"
:min="0"
:max="100"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter>%</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="审核方式">
<a-select v-model:value="configData.auditType" style="width: 100%">
<a-select-option :value="1">自动审核</a-select-option>
<a-select-option :value="2">人工审核</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 等级配置 -->
<div v-if="form.key === 'level_config'" class="level-config">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="升级条件">
<a-select v-model:value="configData.upgradeType" style="width: 100%">
<a-select-option :value="1">按推广人数</a-select-option>
<a-select-option :value="2">按累计佣金</a-select-option>
<a-select-option :value="3">按订单数量</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="升级阈值">
<a-input-number
v-model:value="configData.upgradeThreshold"
:min="0"
placeholder="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 奖励配置 -->
<div v-if="form.key === 'reward_config'" class="reward-config">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="推广奖励">
<a-input-number
v-model:value="configData.promotionReward"
:min="0"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="首单奖励">
<a-input-number
v-model:value="configData.firstOrderReward"
:min="0"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="月度奖励">
<a-input-number
v-model:value="configData.monthlyReward"
:min="0"
:precision="2"
placeholder="0.00"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
</div>
</div>
<!-- JSON 编辑器 -->
<a-form-item label="配置内容" name="values">
<div class="json-editor-container">
<div class="json-editor-header">
<span>JSON 配置</span>
<a-space>
<a-button size="small" @click="formatJson">
<template #icon>
<FormatPainterOutlined />
</template>
格式化
</a-button>
<a-button size="small" @click="validateJson">
<template #icon>
<CheckCircleOutlined />
</template>
验证
</a-button>
<a-button size="small" @click="resetToTemplate" v-if="form.key && form.key !== 'other'">
<template #icon>
<ReloadOutlined />
</template>
重置为模板
</a-button>
</a-space>
</div>
<a-textarea
v-model:value="form.values"
placeholder="请输入JSON格式的配置内容"
:rows="12"
class="json-editor"
@blur="onJsonBlur"
/>
<div class="json-status" v-if="jsonStatus">
<a-alert
:type="jsonStatus.type"
:message="jsonStatus.message"
show-icon
:closable="false"
/>
</div>
</div>
</a-form-item>
</a-form>
</ele-modal>
@@ -47,21 +298,19 @@
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import {
FormatPainterOutlined,
CheckCircleOutlined,
ReloadOutlined
} from '@ant-design/icons-vue';
import { assignObject } from 'ele-admin-pro';
import { addShopDealerSetting, updateShopDealerSetting } from '@/api/shop/shopDealerSetting';
import { ShopDealerSetting } from '@/api/shop/shopDealerSetting/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<{
// 弹窗是否打开
@@ -81,22 +330,38 @@
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
// 表单数据
const form = reactive<ShopDealerSetting>({
key: undefined,
describe: undefined,
values: undefined,
describe: '',
values: '',
tenantId: undefined,
updateTime: undefined,
shopDealerSettingId: undefined,
shopDealerSettingName: '',
status: 0,
comments: '',
sortNumber: 100
updateTime: undefined
});
// 配置数据(用于模板配置)
const configData = reactive<any>({
// 佣金比例配置
firstRate: 0,
secondRate: 0,
thirdRate: 0,
// 提现配置
minAmount: 0,
feeRate: 0,
auditType: 1,
// 等级配置
upgradeType: 1,
upgradeThreshold: 0,
// 奖励配置
promotionReward: 0,
firstOrderReward: 0,
monthlyReward: 0
});
// JSON状态
const jsonStatus = ref<any>(null);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
@@ -104,28 +369,180 @@
// 表单验证规则
const rules = reactive({
shopDealerSettingName: [
key: [
{
required: true,
type: 'string',
message: '请填写分销商设置表名称',
message: '请选择设置标识',
trigger: 'change'
}
],
describe: [
{
required: true,
message: '请输入设置描述',
trigger: 'blur'
},
{
min: 2,
max: 100,
message: '描述长度应在2-100个字符之间',
trigger: 'blur'
}
],
values: [
{
required: true,
message: '请输入配置内容',
trigger: 'blur'
},
{
validator: (rule: any, value: any) => {
if (value) {
try {
JSON.parse(value);
return Promise.resolve();
} catch (e) {
return Promise.reject('配置内容必须是有效的JSON格式');
}
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
/* 获取模板标题 */
const getTemplateTitle = () => {
const titleMap = {
commission_rate: '佣金比例配置模板',
withdraw_config: '提现配置模板',
level_config: '等级配置模板',
reward_config: '奖励配置模板'
};
return titleMap[form.key] || '配置模板';
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
/* 获取模板描述 */
const getTemplateDescription = () => {
const descMap = {
commission_rate: '设置一级、二级、三级分销商的佣金比例,支持小数点后两位',
withdraw_config: '配置提现的最小金额、手续费比例和审核方式',
level_config: '设置分销商等级升级的条件和阈值',
reward_config: '配置推广奖励、首单奖励和月度奖励金额'
};
return descMap[form.key] || '请根据业务需求配置相关参数';
};
/* 设置标识改变时的处理 */
const onSettingKeyChange = (value: string) => {
// 重置配置数据
Object.keys(configData).forEach(key => {
configData[key] = typeof configData[key] === 'number' ? 0 : '';
});
// 设置默认描述
const descMap = {
commission_rate: '分销佣金比例设置',
withdraw_config: '提现相关参数配置',
level_config: '分销商等级配置',
reward_config: '推广奖励配置',
other: '自定义设置项'
};
if (!form.describe) {
form.describe = descMap[value] || '';
}
// 生成默认JSON
resetToTemplate();
};
/* 重置为模板 */
const resetToTemplate = () => {
if (!form.key || form.key === 'other') {
form.values = '{}';
return;
}
let template = {};
switch (form.key) {
case 'commission_rate':
template = {
firstRate: configData.firstRate || 10,
secondRate: configData.secondRate || 5,
thirdRate: configData.thirdRate || 2,
description: '分销佣金比例配置'
};
break;
case 'withdraw_config':
template = {
minAmount: configData.minAmount || 100,
feeRate: configData.feeRate || 1,
auditType: configData.auditType || 1,
description: '提现配置参数'
};
break;
case 'level_config':
template = {
upgradeType: configData.upgradeType || 1,
upgradeThreshold: configData.upgradeThreshold || 10,
description: '分销商等级配置'
};
break;
case 'reward_config':
template = {
promotionReward: configData.promotionReward || 10,
firstOrderReward: configData.firstOrderReward || 5,
monthlyReward: configData.monthlyReward || 50,
description: '推广奖励配置'
};
break;
}
form.values = JSON.stringify(template, null, 2);
validateJson();
};
/* 格式化JSON */
const formatJson = () => {
try {
const parsed = JSON.parse(form.values);
form.values = JSON.stringify(parsed, null, 2);
jsonStatus.value = {
type: 'success',
message: 'JSON格式化成功'
};
} catch (e) {
jsonStatus.value = {
type: 'error',
message: 'JSON格式错误无法格式化'
};
}
};
/* 验证JSON */
const validateJson = () => {
try {
JSON.parse(form.values);
jsonStatus.value = {
type: 'success',
message: 'JSON格式正确'
};
} catch (e) {
jsonStatus.value = {
type: 'error',
message: `JSON格式错误: ${e.message}`
};
}
};
/* JSON失焦时验证 */
const onJsonBlur = () => {
if (form.values) {
validateJson();
}
};
const { resetFields } = useForm(form, rules);
@@ -135,13 +552,26 @@
if (!formRef.value) {
return;
}
// 先验证JSON格式
if (form.values) {
try {
JSON.parse(form.values);
} catch (e) {
message.error('配置内容JSON格式错误请检查后重试');
return;
}
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
...form,
updateTime: Date.now()
};
const saveOrUpdate = isUpdate.value ? updateShopDealerSetting : addShopDealerSetting;
saveOrUpdate(formData)
.then((msg) => {
@@ -162,18 +592,40 @@
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
jsonStatus.value = null;
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
// 解析配置数据到模板
if (props.data.values) {
try {
const parsed = JSON.parse(props.data.values);
Object.keys(configData).forEach(key => {
if (parsed[key] !== undefined) {
configData[key] = parsed[key];
}
});
} catch (e) {
console.warn('解析配置数据失败:', e);
}
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
key: undefined,
describe: '',
values: '{}',
tenantId: undefined,
updateTime: undefined
});
// 重置配置数据
Object.keys(configData).forEach(key => {
configData[key] = typeof configData[key] === 'number' ? 0 : '';
});
isUpdate.value = false;
}
} else {
@@ -182,4 +634,111 @@
},
{ immediate: true }
);
// 监听配置数据变化自动更新JSON
watch(
() => configData,
() => {
if (form.key && form.key !== 'other') {
resetToTemplate();
}
},
{ deep: true }
);
</script>
<style lang="less" scoped>
.setting-option {
display: flex;
align-items: center;
.ant-tag {
margin-right: 8px;
}
span {
color: #666;
font-size: 12px;
}
}
.config-template {
background: #fafafa;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
.commission-config,
.withdraw-config,
.level-config,
.reward-config {
margin-top: 16px;
}
}
.json-editor-container {
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
.json-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #fafafa;
border-bottom: 1px solid #d9d9d9;
span {
font-weight: 600;
color: #333;
}
}
.json-editor {
border: none;
border-radius: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.5;
&:focus {
box-shadow: none;
}
}
.json-status {
padding: 8px 12px;
border-top: 1px solid #d9d9d9;
background: #fff;
}
}
: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-select-selection-item) {
display: flex;
align-items: center;
}
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 600;
}
}
</style>
</script>

View File

@@ -1,217 +1,329 @@
<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="shopDealerSettingId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '24px' }">
<!-- 设置标签页 -->
<a-tabs v-model:activeKey="activeTab" type="card" class="setting-tabs">
<a-tab-pane key="basic" tab="基础设置">
<a-form
:model="basicSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 是否开启分销功能 -->
<a-form-item label="是否开启分销功能">
<a-radio-group v-model:value="basicSettings.enableDistribution">
<a-radio :value="true">开启</a-radio>
<a-radio :value="false">关闭</a-radio>
</a-radio-group>
<div class="setting-desc">开启后用户可以申请成为分销商</div>
</a-form-item>
<!-- 编辑弹窗 -->
<ShopDealerSettingEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 分销层级 -->
<a-form-item label="分销层级">
<a-radio-group v-model:value="basicSettings.distributionLevel">
<a-radio :value="1">一级</a-radio>
<a-radio :value="2">二级</a-radio>
<a-radio :value="3">三级</a-radio>
</a-radio-group>
<div class="setting-desc">设置分销商推荐层级关系</div>
</a-form-item>
<!-- 分销商内购 -->
<a-form-item label="分销商内购">
<a-radio-group v-model:value="basicSettings.dealerSelfBuy">
<a-radio :value="true">开启</a-radio>
<a-radio :value="false">关闭</a-radio>
</a-radio-group>
<div class="setting-desc">分销商自己购买是否获得佣金开启一般佣金</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="commission" tab="分销条件">
<a-form
:model="commissionSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 申请方式 -->
<a-form-item label="申请方式">
<a-radio-group v-model:value="commissionSettings.applyType">
<a-radio :value="10">需后台审核</a-radio>
<a-radio :value="20">无需审核</a-radio>
</a-radio-group>
<div class="setting-desc">设置用户申请分销商的审核方式</div>
</a-form-item>
<!-- 佣金结算 -->
<a-form-item label="佣金结算">
<a-radio-group v-model:value="commissionSettings.settlementType">
<a-radio :value="10">订单完成</a-radio>
<a-radio :value="20">订单确认收货</a-radio>
</a-radio-group>
<div class="setting-desc">设置佣金何时结算到分销商账户</div>
</a-form-item>
<!-- 最低提现金额 -->
<a-form-item label="最低提现金额">
<a-input-number
v-model:value="commissionSettings.minWithdrawAmount"
:min="0"
:precision="2"
style="width: 200px"
>
<template #addonAfter></template>
</a-input-number>
<div class="setting-desc">分销商申请提现的最低金额限制</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="withdraw" tab="提现设置">
<a-form
:model="withdrawSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 提现方式 -->
<a-form-item label="提现方式">
<a-checkbox-group v-model:value="withdrawSettings.withdrawMethods">
<a-checkbox :value="10">微信</a-checkbox>
<a-checkbox :value="20">支付宝</a-checkbox>
<a-checkbox :value="30">银行卡</a-checkbox>
</a-checkbox-group>
<div class="setting-desc">设置支持的提现方式</div>
</a-form-item>
<!-- 提现手续费 -->
<a-form-item label="提现手续费">
<a-input-number
v-model:value="withdrawSettings.withdrawFeeRate"
:min="0"
:max="100"
:precision="2"
style="width: 200px"
>
<template #addonAfter>%</template>
</a-input-number>
<div class="setting-desc">提现时收取的手续费比例</div>
</a-form-item>
<!-- 提现审核 -->
<a-form-item label="提现审核">
<a-radio-group v-model:value="withdrawSettings.withdrawAudit">
<a-radio :value="true">需要审核</a-radio>
<a-radio :value="false">无需审核</a-radio>
</a-radio-group>
<div class="setting-desc">设置提现申请是否需要人工审核</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="agreement" tab="协议">
<a-form
:model="agreementSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 分销商协议 -->
<a-form-item label="分销商协议">
<a-textarea
v-model:value="agreementSettings.dealerAgreement"
:rows="10"
placeholder="请输入分销商协议内容..."
/>
<div class="setting-desc">用户申请分销商时需要同意的协议内容</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="notification" tab="自定义文字">
<a-form
:model="notificationSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 申请成功提示 -->
<a-form-item label="申请成功提示">
<a-textarea
v-model:value="notificationSettings.applySuccessText"
:rows="3"
placeholder="请输入申请成功后的提示文字..."
/>
</a-form-item>
<!-- 申请失败提示 -->
<a-form-item label="申请失败提示">
<a-textarea
v-model:value="notificationSettings.applyFailText"
:rows="3"
placeholder="请输入申请失败后的提示文字..."
/>
</a-form-item>
<!-- 提现成功提示 -->
<a-form-item label="提现成功提示">
<a-textarea
v-model:value="notificationSettings.withdrawSuccessText"
:rows="3"
placeholder="请输入提现成功后的提示文字..."
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="page" tab="页面设置">
<a-form
:model="pageSettings"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<!-- 分销中心标题 -->
<a-form-item label="分销中心标题">
<a-input
v-model:value="pageSettings.centerTitle"
placeholder="请输入分销中心页面标题"
/>
</a-form-item>
<!-- 分销中心背景图 -->
<a-form-item label="分销中心背景图">
<a-upload
v-model:file-list="pageSettings.backgroundImages"
list-type="picture-card"
:max-count="1"
@preview="handlePreview"
>
<div v-if="pageSettings.backgroundImages.length < 1">
<PlusOutlined />
<div style="margin-top: 8px">上传</div>
</div>
</a-upload>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
<!-- 保存按钮 -->
<div class="setting-footer">
<a-button type="primary" size="large" @click="saveSettings" :loading="saving">
保存设置
</a-button>
</div>
</a-card>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } 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 ShopDealerSettingEdit from './components/shopDealerSettingEdit.vue';
import { pageShopDealerSetting, removeShopDealerSetting, removeBatchShopDealerSetting } from '@/api/shop/shopDealerSetting';
import type { ShopDealerSetting, ShopDealerSettingParam } from '@/api/shop/shopDealerSetting/model';
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { getPageTitle } from '@/utils/common';
import { updateShopDealerSetting, getShopDealerSetting } from '@/api/shop/shopDealerSetting';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 当前激活的标签页
const activeTab = ref('basic');
// 保存状态
const saving = ref(false);
// 表格选中数据
const selection = ref<ShopDealerSetting[]>([]);
// 当前编辑数据
const current = ref<ShopDealerSetting | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 基础设置
const basicSettings = reactive({
enableDistribution: true,
distributionLevel: 3,
dealerSelfBuy: false
});
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
// 分销条件设置
const commissionSettings = reactive({
applyType: 10,
settlementType: 10,
minWithdrawAmount: 100
});
// 提现设置
const withdrawSettings = reactive({
withdrawMethods: [10, 20, 30],
withdrawFeeRate: 0,
withdrawAudit: true
});
// 协议设置
const agreementSettings = reactive({
dealerAgreement: '分销商协议内容...'
});
// 通知设置
const notificationSettings = reactive({
applySuccessText: '恭喜您成功成为分销商!',
applyFailText: '很抱歉,您的申请未通过审核。',
withdrawSuccessText: '提现申请已提交,请耐心等待处理。'
});
// 页面设置
const pageSettings = reactive({
centerTitle: '分销中心',
backgroundImages: []
});
/* 图片预览 */
const handlePreview = (file: any) => {
console.log('预览图片:', file);
};
/* 加载设置 */
const loadSettings = async () => {
try {
// 这里应该调用API获取设置数据
// const settings = await getShopDealerSetting();
// 然后将数据分配到各个设置对象中
console.log('加载设置数据');
} catch (error) {
console.error('加载设置失败:', error);
message.error('加载设置失败');
}
return pageShopDealerSetting({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '设置项标示',
dataIndex: 'key',
key: 'key',
align: 'center',
width: 90,
},
{
title: '设置项描述',
dataIndex: 'describe',
key: 'describe',
align: 'center',
},
{
title: '设置内容(json格式)',
dataIndex: 'values',
key: 'values',
align: 'center',
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
/* 保存设置 */
const saveSettings = async () => {
saving.value = true;
try {
// 收集所有设置数据
const allSettings = {
basic: basicSettings,
commission: commissionSettings,
withdraw: withdrawSettings,
agreement: agreementSettings,
notification: notificationSettings,
page: pageSettings
};
console.log('保存设置:', allSettings);
// 这里应该调用API保存设置
// await updateShopDealerSetting(allSettings);
// 模拟保存
await new Promise(resolve => setTimeout(resolve, 1000));
message.success('设置保存成功');
} catch (error) {
console.error('保存设置失败:', error);
message.error('保存设置失败');
} finally {
saving.value = false;
}
]);
/* 搜索 */
const reload = (where?: ShopDealerSettingParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerSetting) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerSetting) => {
const hide = message.loading('请求中..', 0);
removeShopDealerSetting(row.shopDealerSettingId)
.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);
removeBatchShopDealerSetting(selection.value.map((d) => d.shopDealerSettingId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerSetting) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
// 页面加载时获取设置数据
onMounted(() => {
loadSettings();
});
</script>
<script lang="ts">
@@ -220,4 +332,72 @@
};
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.dealer-setting-container {
max-width: 1200px;
margin: 0 auto;
}
.setting-tabs {
:deep(.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab) {
border-radius: 6px 6px 0 0;
background: #fafafa;
border: 1px solid #d9d9d9;
margin-right: 8px;
&.ant-tabs-tab-active {
background: #fff;
border-bottom-color: #fff;
}
}
:deep(.ant-tabs-content-holder) {
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 0 6px 6px 6px;
padding: 24px;
min-height: 500px;
}
}
.setting-desc {
color: #999;
font-size: 12px;
margin-top: 4px;
line-height: 1.4;
}
.setting-footer {
text-align: center;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
:deep(.ant-form-item-label > label) {
font-weight: 500;
}
:deep(.ant-radio-group) {
.ant-radio-wrapper {
margin-right: 16px;
}
}
:deep(.ant-checkbox-group) {
.ant-checkbox-wrapper {
margin-right: 16px;
margin-bottom: 8px;
}
}
:deep(.ant-upload-select-picture-card) {
width: 120px;
height: 120px;
}
:deep(.ant-upload-list-picture-card .ant-upload-list-item) {
width: 120px;
height: 120px;
}
</style>

View File

@@ -0,0 +1,110 @@
<!-- 分销商用户导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="分销商用户批量导入"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件导入模板和导出模板格式一致</span>
</div>
<div class="import-tips" style="margin-top: 16px;">
<a-alert
message="导入说明"
type="info"
show-icon
>
<template #description>
<div>
<p>1. 请按照导出的Excel格式准备数据</p>
<p>2. 必填字段用户ID姓名手机号</p>
<p>3. 佣金字段请填写数字不要包含货币符号</p>
<p>4. 状态字段正常 已删除</p>
<p>5. 推荐人ID必须是已存在的用户ID</p>
</div>
</template>
</a-alert>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import {importShopDealerUsers} from "@/api/shop/shopDealerUser";
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
// 是否打开弹窗
visible: boolean;
}>();
// 导入请求状态
const loading = ref(false);
/* 上传 */
const doUpload = ({ file }) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importShopDealerUsers(file)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>
<style lang="less" scoped>
.import-tips {
:deep(.ant-alert-description) {
p {
margin: 4px 0;
font-size: 13px;
}
}
}
</style>

View File

@@ -1,42 +1,219 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
<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.realName"
placeholder="请输入申请人姓名"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="手机号码">
<a-input
v-model:value="searchForm.mobile"
placeholder="请输入手机号码"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="申请方式">
<a-select
v-model:value="searchForm.applyType"
placeholder="全部方式"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">需要审核</a-select-option>
<a-select-option :value="20">免审核</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审核状态">
<a-select
v-model:value="searchForm.applyStatus"
placeholder="全部状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">待审核</a-select-option>
<a-select-option :value="20">审核通过</a-select-option>
<a-select-option :value="30">审核驳回</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="申请时间">
<a-range-picker
v-model:value="searchForm.dateRange"
style="width: 240px"
/>
</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" @click="add" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- 新增申请-->
<!-- </a-button>-->
<a-button
type="primary"
ghost
:disabled="!selection?.length"
@click="batchApprove"
class="ele-btn-icon"
>
<template #icon>
<CheckOutlined />
</template>
批量通过
</a-button>
<a-button
:disabled="!selection?.length"
@click="exportData"
class="ele-btn-icon"
>
<template #icon>
<ExportOutlined />
</template>
导出数据
</a-button>
</a-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import { reactive } from 'vue';
import {
PlusOutlined,
SearchOutlined,
CheckOutlined,
ExportOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerApplyParam } from '@/api/shop/shopDealerApply/model';
import dayjs from 'dayjs';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
// 选中的数据
selection?: any[];
}>(),
{}
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'search', where?: ShopDealerApplyParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'batchApprove'): void;
(e: 'export'): void;
}>();
// 搜索表单
const searchForm = reactive<any>({
realName: '',
mobile: '',
applyType: undefined,
applyStatus: undefined,
dateRange: undefined
});
// 搜索
const handleSearch = () => {
const searchParams: ShopDealerApplyParam = {};
if (searchForm.realName) {
searchParams.realName = searchForm.realName;
}
if (searchForm.mobile) {
searchParams.mobile = searchForm.mobile;
}
if (searchForm.applyType) {
searchParams.applyType = searchForm.applyType;
}
if (searchForm.applyStatus) {
searchParams.applyStatus = searchForm.applyStatus;
}
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
searchParams.startTime = dayjs(searchForm.dateRange[0]).format('YYYY-MM-DD');
searchParams.endTime = dayjs(searchForm.dateRange[1]).format('YYYY-MM-DD');
}
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
searchForm.realName = '';
searchForm.mobile = '';
searchForm.applyType = undefined;
searchForm.applyStatus = undefined;
searchForm.dateRange = undefined;
emit('search', {});
};
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
// 批量通过
const batchApprove = () => {
emit('batchApprove');
};
// 导出数据
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,407 @@
<!-- 编辑弹窗 -->
<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"
:disabled="isUpdate"
v-model:value="form.userId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="真实姓名" name="realName">
<a-input
placeholder="请输入真实姓名"
v-model:value="form.realName"
:disabled="isUpdate"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="手机号码" name="mobile">
<a-input
placeholder="请输入手机号码"
:disabled="isUpdate"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="推荐人ID" name="refereeId">
<a-input-number
:min="1"
placeholder="请输入推荐人用户ID"
:disabled="isUpdate"
v-model:value="form.refereeId"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 审核信息 -->
<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="applyStatus">
<a-select v-model:value="form.applyStatus" placeholder="请选择审核状态" @change="handleStatusChange">
<a-select-option :value="10">
<a-tag color="processing">待审核</a-tag>
<span style="margin-left: 8px;">等待审核</span>
</a-select-option>
<a-select-option :value="20">
<a-tag color="success">审核通过</a-tag>
<span style="margin-left: 8px;">申请通过</span>
</a-select-option>
<a-select-option :value="30">
<a-tag color="error">审核驳回</a-tag>
<span style="margin-left: 8px;">申请驳回</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="审核时间" name="auditTime" v-if="form.applyStatus === 20 || form.applyStatus === 30">-->
<!-- <a-date-picker-->
<!-- v-model:value="form.auditTime"-->
<!-- show-time-->
<!-- format="YYYY-MM-DD HH:mm:ss"-->
<!-- placeholder="请选择审核时间"-->
<!-- style="width: 100%"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<a-row :gutter="16" v-if="form.applyStatus === 30">
<a-col :span="24">
<a-form-item label="驳回原因" name="rejectReason">
<a-textarea
v-model:value="form.rejectReason"
placeholder="请输入驳回原因"
style="width: 100%"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
</a-col>
</a-row>
</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 { addShopDealerApply, updateShopDealerApply } from '@/api/shop/shopDealerApply';
import { ShopDealerApply } from '@/api/shop/shopDealerApply/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?: ShopDealerApply | 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<ShopDealerApply>({
applyId: undefined,
userId: undefined,
realName: '',
mobile: '',
refereeId: undefined,
applyType: 10,
applyTime: undefined,
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
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'
}
],
realName: [
{
required: true,
message: '请输入真实姓名',
trigger: 'blur'
},
{
min: 2,
max: 20,
message: '姓名长度应在2-20个字符之间',
trigger: 'blur'
}
],
mobile: [
{
required: true,
message: '请输入手机号码',
trigger: 'blur'
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
],
applyType: [
{
required: true,
message: '请选择申请方式',
trigger: 'change'
}
],
applyStatus: [
{
required: true,
message: '请选择审核状态',
trigger: 'change'
}
],
rejectReason: [
{
required: true,
message: '驳回时必须填写驳回原因',
trigger: 'blur'
}
],
auditTime: [
{
required: true,
message: '审核时请选择审核时间',
trigger: 'change'
}
]
});
const { resetFields } = useForm(form, rules);
/* 处理审核状态变化 */
const handleStatusChange = (value: number) => {
// 当状态改为审核通过或驳回时,自动设置审核时间为当前时间
if ((value === 20 || value === 30) && !form.auditTime) {
form.auditTime = dayjs();
}
// 当状态改为待审核时,清空审核时间和驳回原因
if (value === 10) {
form.auditTime = undefined;
form.rejectReason = '';
}
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
// 动态验证规则
const validateFields: string[] = ['userId', 'realName', 'mobile', 'applyStatus'];
// 如果是驳回状态,需要验证驳回原因
if (form.applyStatus === 30) {
validateFields.push('rejectReason');
}
// 如果是审核通过或驳回状态,需要验证审核时间
if (form.applyStatus === 20 || form.applyStatus === 30) {
validateFields.push('auditTime');
}
formRef.value
.validate(validateFields)
.then(() => {
loading.value = true;
const formData = {
...form
};
// 处理时间字段转换 - 转换为ISO字符串格式
if (formData.applyTime) {
if (dayjs.isDayjs(formData.applyTime)) {
formData.applyTime = formData.applyTime.format('YYYY-MM-DD HH:mm:ss');
} else if (typeof formData.applyTime === 'number') {
formData.applyTime = dayjs(formData.applyTime).format('YYYY-MM-DD HH:mm:ss');
}
}
if (formData.auditTime) {
if (dayjs.isDayjs(formData.auditTime)) {
formData.auditTime = formData.auditTime.format('YYYY-MM-DD HH:mm:ss');
} else if (typeof formData.auditTime === 'number') {
formData.auditTime = dayjs(formData.auditTime).format('YYYY-MM-DD HH:mm:ss');
}
}
// 当审核状态为通过或驳回时,确保有审核时间
if ((formData.applyStatus === 20 || formData.applyStatus === 30) && !formData.auditTime) {
formData.auditTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
}
// 当状态为待审核时,清空审核时间
if (formData.applyStatus === 10) {
formData.auditTime = undefined;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerApply : addShopDealerApply;
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);
// 处理时间字段 - 确保转换为dayjs对象
if (props.data.applyTime) {
form.applyTime = dayjs(props.data.applyTime);
}
if (props.data.auditTime) {
form.auditTime = dayjs(props.data.auditTime);
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
applyId: undefined,
userId: undefined,
realName: '',
mobile: '',
refereeId: undefined,
applyType: 10,
applyTime: dayjs(),
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
: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-radio) {
display: flex;
align-items: center;
margin-bottom: 8px;
.ant-radio-inner {
margin-right: 8px;
}
}
:deep(.ant-select-selection-item) {
display: flex;
align-items: center;
}
</style>

View File

@@ -1,12 +1,12 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销商用户记录表' : '添加分销商用户记录表'"
:body-style="{ paddingBottom: '28px' }"
:title="isUpdate ? '编辑分销商用户' : '添加分销商用户'"
:body-style="{ paddingBottom: '28px', maxHeight: '70vh', overflowY: 'auto' }"
@update:visible="updateVisible"
@ok="save"
>
@@ -14,109 +14,247 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<a-form-item label="自增ID" name="userId">
<a-input
allow-clear
placeholder="请输入自增ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="姓名" name="realName">
<a-input
allow-clear
placeholder="请输入姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
placeholder="请输入手机号"
v-model:value="form.mobile"
/>
</a-form-item>
<a-form-item label="支付密码" name="payPassword">
<a-input
allow-clear
placeholder="请输入支付密码"
v-model:value="form.payPassword"
/>
</a-form-item>
<a-form-item label="当前可提现佣金" name="money">
<a-input
allow-clear
placeholder="请输入当前可提现佣金"
v-model:value="form.money"
/>
</a-form-item>
<a-form-item label="已冻结佣金" name="freezeMoney">
<a-input
allow-clear
placeholder="请输入已冻结佣金"
v-model:value="form.freezeMoney"
/>
</a-form-item>
<a-form-item label="累积提现佣金" name="totalMoney">
<a-input
allow-clear
placeholder="请输入累积提现佣金"
v-model:value="form.totalMoney"
/>
</a-form-item>
<a-form-item label="推荐人用户ID" name="refereeId">
<a-input
allow-clear
placeholder="请输入推荐人用户ID"
v-model:value="form.refereeId"
/>
</a-form-item>
<a-form-item label="成员数量(一级)" name="firstNum">
<a-input
allow-clear
placeholder="请输入成员数量(一级)"
v-model:value="form.firstNum"
/>
</a-form-item>
<a-form-item label="成员数量(二级)" name="secondNum">
<a-input
allow-clear
placeholder="请输入成员数量(二级)"
v-model:value="form.secondNum"
/>
</a-form-item>
<a-form-item label="成员数量(三级)" name="thirdNum">
<a-input
allow-clear
placeholder="请输入成员数量(三级)"
v-model:value="form.thirdNum"
/>
</a-form-item>
<a-form-item label="专属二维码" name="qrcode">
<a-input
allow-clear
placeholder="请输入专属二维码"
v-model:value="form.qrcode"
/>
</a-form-item>
<a-form-item label="是否删除" name="isDelete">
<a-input
allow-clear
placeholder="请输入是否删除"
v-model:value="form.isDelete"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
<!-- 基本信息 -->
<a-divider orientation="left">基本信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="用户ID"
name="userId"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input-number
:min="1"
placeholder="请输入用户ID"
v-model:value="form.userId"
style="width: 100%"
:disabled="isUpdate"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="姓名"
name="realName"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input
allow-clear
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="手机号"
name="mobile"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input
allow-clear
placeholder="请输入手机号"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="支付密码"
name="payPassword"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input-password
allow-clear
placeholder="请输入支付密码"
v-model:value="form.payPassword"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 佣金信息 -->
<a-divider orientation="left">佣金信息</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item
label="可提现佣金"
name="money"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
:precision="2"
placeholder="0.00"
v-model:value="form.money"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="冻结佣金"
name="freezeMoney"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
:precision="2"
placeholder="0.00"
v-model:value="form.freezeMoney"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="累计提现"
name="totalMoney"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
:precision="2"
placeholder="0.00"
v-model:value="form.totalMoney"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<!-- 推荐关系 -->
<a-divider orientation="left">推荐关系</a-divider>
<a-form-item label="推荐人" name="refereeId">
<a-input-group compact>
<a-input-number
:min="1"
placeholder="请输入推荐人用户ID"
v-model:value="form.refereeId"
style="width: calc(100% - 80px)"
/>
<a-button type="primary" @click="selectReferee">选择</a-button>
</a-input-group>
</a-form-item>
<!-- 团队信息 -->
<a-divider orientation="left">团队信息</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item
label="一级成员"
name="firstNum"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
placeholder="0"
v-model:value="form.firstNum"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="二级成员"
name="secondNum"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
placeholder="0"
v-model:value="form.secondNum"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
label="三级成员"
name="thirdNum"
:label-col="{ span: 12 }"
:wrapper-col="{ span: 12 }"
>
<a-input-number
:min="0"
placeholder="0"
v-model:value="form.thirdNum"
style="width: 100%"
:disabled="true"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<!-- 其他设置 -->
<a-divider orientation="left">其他设置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="专属二维码"
name="qrcode"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-input-group compact>
<a-input
placeholder="系统自动生成"
v-model:value="form.qrcode"
style="width: calc(100% - 80px)"
:disabled="true"
/>
<a-button type="primary" @click="generateQrcode">生成</a-button>
</a-input-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="账户状态"
name="isDelete"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
>
<a-radio-group v-model:value="form.isDelete">
<a-radio :value="0">正常</a-radio>
<a-radio :value="1">已删除</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
@@ -193,11 +331,51 @@
// 表单验证规则
const rules = reactive({
shopDealerUserName: [
userId: [
{
required: true,
type: 'string',
message: '请填写分销商用户记录表名称',
message: '请输入用户ID',
trigger: 'blur'
}
],
realName: [
{
required: true,
message: '请输入真实姓名',
trigger: 'blur'
},
{
min: 2,
max: 20,
message: '姓名长度应在2-20个字符之间',
trigger: 'blur'
}
],
mobile: [
{
required: true,
message: '请输入手机号',
trigger: 'blur'
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号格式',
trigger: 'blur'
}
],
money: [
{
type: 'number',
min: 0,
message: '可提现佣金不能小于0',
trigger: 'blur'
}
],
freezeMoney: [
{
type: 'number',
min: 0,
message: '冻结佣金不能小于0',
trigger: 'blur'
}
]
@@ -219,6 +397,25 @@
const { resetFields } = useForm(form, rules);
/* 选择推荐人 */
const selectReferee = () => {
message.info('推荐人选择功能待开发');
// 这里可以打开用户选择器
};
/* 生成二维码 */
const generateQrcode = () => {
if (!form.userId) {
message.error('请先填写用户ID');
return;
}
// 生成二维码逻辑
const qrcode = `DEALER_${form.userId}_${Date.now()}`;
form.qrcode = qrcode;
message.success('二维码生成成功');
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
@@ -228,20 +425,37 @@
.validate()
.then(() => {
loading.value = true;
// 数据处理
const formData = {
...form
};
// 确保数值类型正确
if (formData.userId) formData.userId = Number(formData.userId);
if (formData.refereeId) formData.refereeId = Number(formData.refereeId);
if (formData.money !== undefined) formData.money = Number(formData.money);
if (formData.freezeMoney !== undefined) formData.freezeMoney = Number(formData.freezeMoney);
if (formData.totalMoney !== undefined) formData.totalMoney = Number(formData.totalMoney);
if (formData.firstNum !== undefined) formData.firstNum = Number(formData.firstNum);
if (formData.secondNum !== undefined) formData.secondNum = Number(formData.secondNum);
if (formData.thirdNum !== undefined) formData.thirdNum = Number(formData.thirdNum);
if (formData.isDelete !== undefined) formData.isDelete = Number(formData.isDelete);
console.log('提交的数据:', formData);
const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
message.success(msg || '操作成功');
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
console.error('保存失败:', e);
message.error(e.message || '操作失败');
});
})
.catch(() => {});
@@ -272,3 +486,31 @@
{ immediate: true }
);
</script>
<style lang="less" scoped>
:deep(.ant-form-item-label) {
text-align: left !important;
}
:deep(.ant-form-item-label > label) {
text-align: left !important;
}
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
margin: 24px 0 16px 0;
.ant-divider-inner-text {
color: #1890ff;
font-weight: 600;
}
}
:deep(.ant-input-group-addon) {
padding: 0 8px;
}
:deep(.ant-input-number-disabled) {
background-color: #f5f5f5;
color: #999;
}
</style>

View File

@@ -1,298 +1,496 @@
<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="shopDealerUserId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="applyId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
v-model:selection="selection"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@batchApprove="batchApprove"
@export="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applyStatus'">
<a-tag v-if="record.applyStatus === 10" color="orange">待审核</a-tag>
<a-tag v-if="record.applyStatus === 20" color="green">已通过</a-tag>
<a-tag v-if="record.applyStatus === 30" color="red">已驳回</a-tag>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)" class="ele-text-primary">
<EditOutlined/>
编辑
</a>
<template v-if="record.applyStatus !== 20">
<a-divider type="vertical"/>
<a @click="approveApply(record)" class="ele-text-success">
<CheckOutlined/>
通过
</a>
<a-divider type="vertical"/>
<a @click="rejectApply(record)" class="ele-text-warning">
<CloseOutlined/>
驳回
</a>
<a-divider type="vertical"/>
<a-popconfirm
v-if="record.applyStatus != 20"
title="确定要删除此申请记录吗?"
@confirm="remove(record)"
placement="topRight"
>
<a class="ele-text-danger">
<DeleteOutlined/>
删除
</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 编辑弹窗 -->
<ShopDealerApplyEdit 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 } 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 ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {
ExclamationCircleOutlined,
CheckOutlined,
CloseOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} 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 ShopDealerApplyEdit from './components/shopDealerApplyEdit.vue';
import {
pageShopDealerApply,
removeShopDealerApply,
removeBatchShopDealerApply,
batchApproveShopDealerApply,
updateShopDealerApply
} from '@/api/shop/shopDealerApply';
import type {ShopDealerApply, ShopDealerApplyParam} from '@/api/shop/shopDealerApply/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据
const current = ref<ShopDealerUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格选中数据
const selection = ref<ShopDealerApply[]>([]);
// 当前编辑数据
const current = ref<ShopDealerApply | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 4;
where.applyStatus = 20;
return pageShopDealerApply({
...where,
...orders,
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'applyId',
key: 'applyId',
align: 'center',
width: 80,
fixed: 'left'
},
{
title: '申请人信息',
key: 'applicantInfo',
align: 'left',
fixed: 'left',
customRender: ({record}) => {
return `${record.realName || '-'} (${record.mobile || '-'})`;
}
return pageShopDealerUser({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '自增ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '姓名',
dataIndex: 'realName',
key: 'realName',
align: 'center',
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile',
align: 'center',
},
{
title: '支付密码',
dataIndex: 'payPassword',
key: 'payPassword',
align: 'center',
},
{
title: '当前可提现佣金',
dataIndex: 'money',
key: 'money',
align: 'center',
},
{
title: '已冻结佣金',
dataIndex: 'freezeMoney',
key: 'freezeMoney',
align: 'center',
},
{
title: '累积提现佣金',
dataIndex: 'totalMoney',
key: 'totalMoney',
align: 'center',
},
{
title: '推荐人用户ID',
dataIndex: 'refereeId',
key: 'refereeId',
align: 'center',
},
{
title: '成员数量(一级)',
dataIndex: 'firstNum',
key: 'firstNum',
align: 'center',
},
{
title: '成员数量(二级)',
dataIndex: 'secondNum',
key: 'secondNum',
align: 'center',
},
{
title: '成员数量(三级)',
dataIndex: 'thirdNum',
key: 'thirdNum',
align: 'center',
},
{
title: '专属二维码',
dataIndex: 'qrcode',
key: 'qrcode',
align: 'center',
},
{
title: '是否删除',
dataIndex: 'isDelete',
key: 'isDelete',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
},
{
title: '申请方式',
dataIndex: 'applyType',
key: 'applyType',
align: 'center',
width: 120,
customRender: ({text}) => {
const typeMap = {
10: {text: '需审核', color: 'orange'},
20: {text: '免审核', color: 'green'}
};
const type = typeMap[text] || {text: '未知', color: 'default'};
return {type: 'tag', props: {color: type.color}, children: type.text};
}
]);
},
{
title: '审核状态',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
width: 120
},
{
title: '推荐人',
dataIndex: 'refereeId',
key: 'refereeId',
align: 'center',
width: 100,
customRender: ({text}) => text ? `ID: ${text}` : '无'
},
// {
// title: '申请时间',
// dataIndex: 'applyTime',
// key: 'applyTime',
// align: 'center',
// width: 120,
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// },
// {
// title: '审核时间',
// dataIndex: 'auditTime',
// key: 'auditTime',
// align: 'center',
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// },
{
title: '驳回原因',
dataIndex: 'rejectReason',
key: 'rejectReason',
align: 'left',
ellipsis: true,
customRender: ({text}) => text || '-'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true
},
{
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
width: 380,
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopDealerUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 搜索 */
const reload = (where?: ShopDealerApplyParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(row.shopDealerUserId)
.then((msg) => {
/* 审核通过 */
const approveApply = (row: ShopDealerApply) => {
Modal.confirm({
title: '审核通过确认',
content: `确定要通过 ${row.realName} 的经销商申请吗?`,
icon: createVNode(CheckOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
const hide = message.loading('正在处理审核...', 0);
try {
await updateShopDealerApply({
...row,
applyId: row.applyId,
applyStatus: 20
});
hide();
message.success(msg);
message.success('审核通过成功');
reload();
})
.catch((e) => {
} catch (error: any) {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
message.error(error.message || '审核失败,请重试');
}
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerUser(selection.value.map((d) => d.shopDealerUserId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
});
};
/* 审核驳回 */
const rejectApply = (row: ShopDealerApply) => {
let rejectReason = '';
Modal.confirm({
title: '审核驳回',
content: createVNode('div', null, [
createVNode('p', null, `申请人: ${row.realName} (${row.mobile})`),
createVNode('p', {style: 'margin-top: 12px;'}, '请输入驳回原因:'),
createVNode('textarea', {
placeholder: '请输入驳回原因...',
style: 'width: 100%; height: 80px; margin-top: 8px; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
onInput: (e: any) => {
rejectReason = e.target.value;
}
})
]),
icon: createVNode(CloseOutlined),
okText: '确认驳回',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
if (!rejectReason.trim()) {
message.error('请输入驳回原因');
return Promise.reject();
}
const hide = message.loading('正在处理审核...', 0);
try {
await updateShopDealerApply({
...row,
applyStatus: 30,
rejectReason: rejectReason.trim()
});
hide();
message.success('审核驳回成功');
reload();
} catch (error: any) {
hide();
message.error(error.message || '审核失败,请重试');
}
}
});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerApply) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerApply) => {
if (!row.applyId) {
message.error('删除失败:缺少必要参数');
return;
}
const hide = message.loading('正在删除申请记录...', 0);
removeShopDealerApply(row.applyId)
.then((msg) => {
hide();
message.success(msg || '删除成功');
reload();
})
.catch((e) => {
hide();
message.error(e.message || '删除失败');
});
};
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
/* 自定义行属性 */
const customRow = (record: ShopDealerUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
const validIds = selection.value.filter(d => d.applyId).map(d => d.applyId);
if (!validIds.length) {
message.error('选中的数据中没有有效的ID');
return;
}
Modal.confirm({
title: '批量删除确认',
content: `确定要删除选中的 ${validIds.length} 条申请记录吗?此操作不可恢复。`,
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk: () => {
const hide = message.loading(`正在删除 ${validIds.length} 条记录...`, 0);
removeBatchShopDealerApply(validIds)
.then((msg) => {
hide();
message.success(msg || `成功删除 ${validIds.length} 条记录`);
selection.value = [];
reload();
})
.catch((e) => {
hide();
message.error(e.message || '批量删除失败');
});
}
});
};
/* 批量通过 */
const batchApprove = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const pendingApplies = selection.value.filter(item => item.applyStatus === 10);
if (!pendingApplies.length) {
message.error('所选申请中没有待审核的记录');
return;
}
Modal.confirm({
title: '批量通过确认',
content: `确定要通过选中的 ${pendingApplies.length} 个申请吗?`,
icon: createVNode(ExclamationCircleOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
const hide = message.loading('正在批量通过...', 0);
try {
const ids = pendingApplies.map(item => item.applyId);
await batchApproveShopDealerApply(ids);
hide();
message.success(`成功通过 ${pendingApplies.length} 个申请`);
selection.value = [];
reload();
} catch (error: any) {
hide();
message.error(error.message || '批量审核失败,请重试');
}
};
}
});
};
/* 导出数据 */
const exportData = () => {
const hide = message.loading('正在导出申请数据...', 0);
// 这里调用导出API
setTimeout(() => {
hide();
message.success('申请数据导出成功');
}, 2000);
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerApply) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
query();
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerUser'
};
export default {
name: 'ShopDealerApply'
};
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.sys-org-table {
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f8f9fa;
}
}
.detail-item {
p {
margin: 4px 0;
color: #666;
}
strong {
color: #1890ff;
font-size: 14px;
}
}
.ele-text-primary {
color: #1890ff;
&:hover {
color: #40a9ff;
}
}
.ele-text-info {
color: #13c2c2;
&:hover {
color: #36cfc9;
}
}
.ele-text-success {
color: #52c41a;
&:hover {
color: #73d13d;
}
}
.ele-text-warning {
color: #faad14;
&:hover {
color: #ffc53d;
}
}
.ele-text-danger {
color: #ff4d4f;
&:hover {
color: #ff7875;
}
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 经销商订单导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="导入分销订单"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined/>
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
<div class="ant-upload-text text-gray-400">
<div>1必须按<a href="https://oss.wsdns.cn/20251018/408b805ec3cd4084a4dc686e130af578.xlsx" target="_blank">导入模版</a>的格式上传</div>
<div>2导入成功确认结算完成佣金的发放</div>
</div>
</a-spin>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {message} from 'ant-design-vue/es';
import {CloudUploadOutlined} from '@ant-design/icons-vue';
import {importSdyDealerOrder} from "@/api/sdy/sdyDealerOrder";
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
// 是否打开弹窗
visible: boolean;
}>();
// 导入请求状态
const loading = ref(false);
/* 上传 */
const doUpload = ({file}) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importSdyDealerOrder(file)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

View File

@@ -1,42 +1,106 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
<div class="flex items-center gap-20">
<!-- 搜索表单 -->
<a-form
:model="where"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item>
<a-space>
<a-button
danger
class="ele-btn-icon"
v-if="selection.length > 0"
:disabled="selection?.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined/>
</template>
<span>批量删除</span>
</a-button>
</a-space>
</a-form-item>
<a-form-item>
<a-space>
<a-input-search
allow-clear
placeholder="请输入用户ID"
style="width: 240px"
v-model:value="where.keywords"
@search="handleSearch"
/>
<a-button @click="resetSearch">
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<!-- 导入弹窗 -->
<Import v-model:visible="showImport" @done="emit('importDone')"/>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import {ref} from 'vue';
import {
DeleteOutlined
} from '@ant-design/icons-vue';
import Import from './Import.vue';
import useSearch from "@/utils/use-search";
import {ShopDealerWithdrawParam} from "@/api/shop/shopDealerWithdraw/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
withDefaults(
defineProps<{
// 选中的数据
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const emit = defineEmits<{
(e: 'search', where?: ShopDealerWithdrawParam): void;
(e: 'batchSettle'): void;
(e: 'export'): void;
(e: 'importDone'): void;
(e: 'remove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
// 是否显示导入弹窗
const showImport = ref(false);
// 搜索表单
const {where, resetFields} = useSearch<ShopDealerWithdrawParam>({
keywords: '',
});
// 搜索
const handleSearch = () => {
const searchParams = {...where};
// 清除空值
Object.keys(searchParams).forEach(key => {
if (searchParams[key] === '' || searchParams[key] === undefined) {
delete searchParams[key];
}
});
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
resetFields();
emit('search', {});
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -1,11 +1,11 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑分销商提现明细表' : '添加分销商提现明细表'"
:title="isUpdate ? '编辑提现申请' : '新增提现申请'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -14,253 +14,669 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="分销商用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入分销商用户ID"
v-model:value="form.userId"
<!-- 基本信息 -->
<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="comments">
<div class="text-red-500">{{ form.comments }}</div>
</a-form-item>
</a-col>
</a-row>
<!-- 收款信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">收款信息</span>
</a-divider>
<!-- 微信收款信息 -->
<div v-if="form.payType === 10" class="payment-info wechat-info">
<a-alert
message="微信收款信息"
description="请确保微信账号信息准确,以免影响到账"
type="success"
show-icon
style="margin-bottom: 16px"
/>
</a-form-item>
<a-form-item label="提现金额" name="money">
<a-input
allow-clear
placeholder="请输入提现金额"
v-model:value="form.money"
<a-form-item label="微信号" name="wechatAccount">
<a-input
placeholder="请输入微信号"
v-model:value="form.wechatAccount"
/>
</a-form-item>
<a-form-item label="微信昵称" name="wechatName">
<a-input
placeholder="请输入微信昵称"
v-model:value="form.wechatName"
/>
</a-form-item>
</div>
<!-- 支付宝收款信息 -->
<div v-if="form.payType === 20" class="payment-info alipay-info">
<a-alert
message="支付宝收款信息"
description="请确保支付宝账号信息准确,姓名需与实名认证一致"
type="info"
show-icon
style="margin-bottom: 16px"
/>
</a-form-item>
<a-form-item label="打款方式 (10微信 20支付宝 30银行卡)" name="payType">
<a-input
allow-clear
placeholder="请输入打款方式 (10微信 20支付宝 30银行卡)"
v-model:value="form.payType"
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="支付宝姓名" name="alipayName">
<a-input
placeholder="请输入支付宝实名姓名"
disabled
v-model:value="form.alipayName"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="支付宝账号" name="alipayAccount">
<a-input
placeholder="请输入支付宝账号"
disabled
v-model:value="form.alipayAccount"
/>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 银行卡收款信息 -->
<div v-if="form.payType === 30" class="payment-info bank-info">
<a-alert
message="银行卡收款信息"
description="请确保银行卡信息准确,开户名需与身份证姓名一致"
type="warning"
show-icon
style="margin-bottom: 16px"
/>
</a-form-item>
<a-form-item label="支付宝姓名" name="alipayName">
<a-input
allow-clear
placeholder="请输入支付宝姓名"
v-model:value="form.alipayName"
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="开户行名称" name="bankName">
{{ form.bankName }}
</a-form-item>
<a-form-item label="银行开户名" name="bankAccount">
{{ form.bankAccount }}
</a-form-item>
<a-form-item label="银行卡号" name="bankCard">
{{ form.bankCard }}
</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="applyStatus">
<a-select v-model:value="form.applyStatus" :disabled="form.applyStatus == 40 || form.applyStatus == 30"
placeholder="请选择申请状态">
<a-select-option :value="10">
<div class="status-option">
<a-tag color="orange">待审核</a-tag>
<span>等待审核</span>
</div>
</a-select-option>
<a-select-option :value="20">
<div class="status-option">
<a-tag color="success">审核通过</a-tag>
<span>审核通过</span>
</div>
</a-select-option>
<a-select-option :value="30">
<div class="status-option">
<a-tag color="error">审核驳回</a-tag>
<span>审核驳回</span>
</div>
</a-select-option>
<a-select-option :value="40">
<div class="status-option">
<a-tag>已打款</a-tag>
<span>已完成打款</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="驳回原因" name="rejectReason" v-if="form.applyStatus === 30">
<a-textarea
v-model:value="form.rejectReason"
placeholder="请输入驳回原因"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
<a-form-item label="上传支付凭证" name="image" v-if="form.applyStatus === 40">
<SelectFile
:placeholder="`请选择图片`"
:limit="2"
:data="files"
@done="chooseFile"
@del="onDeleteFile"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 提现预览 -->
<div class="withdraw-preview" v-if="form.money && form.payType">
<a-alert
:type="getPreviewAlertType()"
:message="getPreviewText()"
show-icon
style="margin-top: 16px"
/>
</a-form-item>
<a-form-item label="支付宝账号" name="alipayAccount">
<a-input
allow-clear
placeholder="请输入支付宝账号"
v-model:value="form.alipayAccount"
/>
</a-form-item>
<a-form-item label="开户行名称" name="bankName">
<a-input
allow-clear
placeholder="请输入开户行名称"
v-model:value="form.bankName"
/>
</a-form-item>
<a-form-item label="银行开户名" name="bankAccount">
<a-input
allow-clear
placeholder="请输入银行开户名"
v-model:value="form.bankAccount"
/>
</a-form-item>
<a-form-item label="银行卡号" name="bankCard">
<a-input
allow-clear
placeholder="请输入银行卡号"
v-model:value="form.bankCard"
/>
</a-form-item>
<a-form-item label="申请状态 (10待审核 20审核通过 30驳回 40已打款)" name="applyStatus">
<a-input
allow-clear
placeholder="请输入申请状态 (10待审核 20审核通过 30驳回 40已打款)"
v-model:value="form.applyStatus"
/>
</a-form-item>
<a-form-item label="审核时间" name="auditTime">
<a-input
allow-clear
placeholder="请输入审核时间"
v-model:value="form.auditTime"
/>
</a-form-item>
<a-form-item label="驳回原因" name="rejectReason">
<a-input
allow-clear
placeholder="请输入驳回原因"
v-model:value="form.rejectReason"
/>
</a-form-item>
<a-form-item label="来源客户端(APP、H5、小程序等)" name="platform">
<a-input
allow-clear
placeholder="请输入来源客户端(APP、H5、小程序等)"
v-model:value="form.platform"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addShopDealerWithdraw, updateShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw';
import { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/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';
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject, uuid} from 'ele-admin-pro';
import {addShopDealerWithdraw, updateShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw';
import {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model';
import {FormInstance} from 'ant-design-vue/es/form';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import dayjs from 'dayjs';
import {FileRecord} from "@/api/system/file/model";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerWithdraw | null;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerWithdraw | 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 maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 提交状态
const loading = ref<boolean>(false);
const isSuccess = ref<boolean>(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const files = ref<ItemType[]>([]);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<ShopDealerWithdraw>({
id: undefined,
userId: undefined,
money: undefined,
payType: undefined,
alipayName: undefined,
alipayAccount: undefined,
bankName: undefined,
bankAccount: undefined,
bankCard: undefined,
applyStatus: undefined,
auditTime: undefined,
rejectReason: undefined,
platform: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopDealerWithdrawId: undefined,
shopDealerWithdrawName: '',
status: 0,
comments: '',
sortNumber: 100
});
// 表单数据
const form = reactive<ShopDealerWithdraw>({
id: undefined,
userId: undefined,
realName: undefined,
nickname: undefined,
phone: undefined,
avatar: undefined,
money: undefined,
payType: undefined,
// 微信相关
wechatAccount: '',
wechatName: '',
// 支付宝相关
alipayName: '',
alipayAccount: '',
// 银行卡相关
bankName: '',
bankAccount: '',
bankCard: '',
// 审核相关
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
platform: '',
comments: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopDealerWithdrawName: [
{
required: true,
type: 'string',
message: '请填写分销商提现明细表名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
// 表单验证规则
const rules = reactive({
userId: [
{
required: true,
message: '请输入分销商用户ID',
trigger: 'blur'
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateShopDealerWithdraw : addShopDealerWithdraw;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
],
money: [
{
required: true,
message: '请输入提现金额',
trigger: 'blur'
},
{
validator: (rule: any, value: any) => {
if (value && value <= 0) {
return Promise.reject('提现金额必须大于0');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
payType: [
{
required: true,
message: '请选择打款方式',
trigger: 'change'
}
],
platform: [
{
required: true,
message: '请选择来源平台',
trigger: 'change'
}
],
// 微信验证
wechatAccount: [
{
validator: (rule: any, value: any) => {
if (form.payType === 10 && !value) {
return Promise.reject('请输入微信号');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
wechatName: [
{
validator: (rule: any, value: any) => {
if (form.payType === 10 && !value) {
return Promise.reject('请输入微信昵称');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
// 支付宝验证
alipayName: [
{
validator: (rule: any, value: any) => {
if (form.payType === 20 && !value) {
return Promise.reject('请输入支付宝姓名');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
alipayAccount: [
{
validator: (rule: any, value: any) => {
if (form.payType === 20 && !value) {
return Promise.reject('请输入支付宝账号');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
// 银行卡验证
bankName: [
{
validator: (rule: any, value: any) => {
if (form.payType === 30 && !value) {
return Promise.reject('请输入开户行名称');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
bankAccount: [
{
validator: (rule: any, value: any) => {
if (form.payType === 30 && !value) {
return Promise.reject('请输入银行开户名');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
bankCard: [
{
validator: (rule: any, value: any) => {
if (form.payType === 30 && !value) {
return Promise.reject('请输入银行卡号');
}
if (form.payType === 30 && value && !/^\d{16,19}$/.test(value)) {
return Promise.reject('银行卡号格式不正确');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
applyStatus: [
{
required: true,
message: '请选择申请状态',
trigger: 'change'
}
],
rejectReason: [
{
validator: (rule: any, value: any) => {
if (form.applyStatus === 30 && !value) {
return Promise.reject('驳回时必须填写驳回原因');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
image: [
{
required: true,
message: '请上传打款凭证',
trigger: 'change'
}
]
});
/* 打款方式改变时的处理 */
const onPayTypeChange = (e: any) => {
const payType = e.target.value;
// 清空其他支付方式的信息
if (payType !== 10) {
form.alipayAccount = '';
form.alipayName = '';
}
if (payType !== 20) {
form.alipayName = '';
form.alipayAccount = '';
}
if (payType !== 30) {
form.bankName = '';
form.bankAccount = '';
form.bankCard = '';
}
};
const chooseFile = (data: FileRecord) => {
files.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
form.image = JSON.stringify(files.value.map((d) => d.url));
};
const onDeleteFile = (index: number) => {
files.value.splice(index, 1);
};
/* 获取预览提示类型 */
const getPreviewAlertType = () => {
if (!form.applyStatus) return 'info';
switch (form.applyStatus) {
case 10:
return 'processing';
case 20:
return 'success';
case 30:
return 'error';
case 40:
return 'success';
default:
return 'info';
}
};
/* 获取预览文本 */
const getPreviewText = () => {
if (!form.money || !form.payType) return '';
const amount = parseFloat(form.money.toString()).toFixed(2);
const payTypeMap = {
10: '微信',
20: '支付宝',
30: '银行卡'
};
const statusMap = {
10: '待审核',
20: '审核通过',
30: '审核驳回',
40: '已打款'
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
const payTypeName = payTypeMap[form.payType] || '未知方式';
const statusName = statusMap[form.applyStatus] || '未知状态';
return `提现金额:¥${amount},打款方式:${payTypeName},当前状态:${statusName}`;
};
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
if (isSuccess.value) {
console.log('isSuccess')
updateVisible(false);
emit('done');
return;
}
if (form.realName == '' || form.realName == null) {
message.error('该用户未完成实名认证!');
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
// 处理时间字段转换
if (formData.auditTime && dayjs.isDayjs(formData.auditTime)) {
formData.auditTime = formData.auditTime.valueOf();
}
},
{ immediate: true }
);
// 根据支付方式清理不相关字段
if (formData.payType !== 10) {
delete formData.wechatAccount;
delete formData.wechatName;
}
if (formData.payType !== 20) {
delete formData.alipayName;
delete formData.alipayAccount;
}
if (formData.payType !== 30) {
delete formData.bankName;
delete formData.bankAccount;
delete formData.bankCard;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerWithdraw : addShopDealerWithdraw;
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) {
files.value = [];
if (props.data) {
assignObject(form, props.data);
// 处理时间字段
if (props.data.auditTime) {
form.auditTime = dayjs(props.data.auditTime);
}
if (props.data.image) {
const arr = JSON.parse(props.data.image);
arr.map((url: string) => {
files.value.push({
uid: uuid(),
url: url,
status: 'done'
});
});
isSuccess.value = true;
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
id: undefined,
userId: undefined,
money: undefined,
payType: undefined,
wechatAccount: '',
wechatName: '',
alipayName: '',
alipayAccount: '',
bankName: '',
bankAccount: '',
bankCard: '',
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
platform: '',
image: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>
<style lang="less" scoped>
.platform-option,
.status-option {
display: flex;
align-items: center;
.ant-tag {
margin-right: 8px;
}
span {
color: #666;
font-size: 12px;
}
}
.payment-info {
background: #fafafa;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
&.wechat-info {
border-left: 3px solid #52c41a;
}
&.alipay-info {
border-left: 3px solid #1890ff;
}
&.bank-info {
border-left: 3px solid #faad14;
}
}
.withdraw-preview {
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 600;
font-size: 14px;
}
}
}
: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-radio) {
display: flex;
align-items: center;
margin-bottom: 8px;
.ant-radio-inner {
margin-right: 8px;
}
}
:deep(.ant-select-selection-item) {
display: flex;
align-items: center;
}
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 600;
}
}
</style>

View File

@@ -1,292 +1,439 @@
<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="shopDealerWithdrawId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopDealerWithdrawId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applyStatus'">
<a-tag v-if="record.applyStatus === 10" color="orange">待审核</a-tag>
<a-tag v-if="record.applyStatus === 20" color="success">审核通过</a-tag>
<a-tag v-if="record.applyStatus === 30" color="error">已驳回</a-tag>
<a-tag v-if="record.applyStatus === 40">已打款</a-tag>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
<template v-if="column.key === 'userInfo'">
<a-space>
<a-avatar :src="record.avatar" />
<div class="flex flex-col">
<span>{{ record.realName || '未实名认证' }}</span>
<span class="text-gray-400">{{ record.phone }}</span>
</div>
</a-space>
</template>
<template v-if="column.key === 'paymentInfo'">
<template v-if="record.payType === 10">
<a-space direction="vertical">
<a-tag color="blue">微信</a-tag>
<span>{{ record.wechatName }}</span>
<span>{{ record.wechatName }}</span>
</a-space>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
<template v-if="record.payType === 20">
<a-space direction="vertical">
<a-tag color="blue">支付宝</a-tag>
<span>{{ record.alipayName }}</span>
<span>{{ record.alipayAccount }}</span>
</a-space>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
<template v-if="record.payType === 30">
<a-space direction="vertical">
<a-tag color="blue">银行卡</a-tag>
<span>{{ record.bankName }}</span>
<span>{{ record.bankAccount }}</span>
<span>{{ record.bankCard }}</span>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<template v-if="column.key === 'comments'">
<template v-if="record.applyStatus === 30">
<div class="text-red-500">驳回原因{{ record.rejectReason }}</div>
</template>
<template v-if="record.applyStatus === 40 && record.image">
<a-image v-for="(item,index) in JSON.parse(record.image)" :key="index" :src="item" :width="50"
:height="50"/>
</template>
</template>
<template v-if="column.key === 'createTime'">
<a-space direction="vertical">
<a-tooltip title="创建时间">{{ record.createTime }}</a-tooltip>
<a-tooltip title="审核/打款时间" class="text-green-500">{{ record.auditTime }}</a-tooltip>
</a-space>
</template>
<template v-if="column.key === 'action'">
<template v-if="record.applyStatus !== 40">
<a @click="openEdit(record)" class="ele-text-primary">
<EditOutlined/>
编辑
</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此提现记录吗?"
@confirm="remove(record)"
placement="topRight"
>
<a class="ele-text-danger">
<DeleteOutlined/>
删除
</a>
</a-popconfirm>
</template>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopDealerWithdrawEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 编辑弹窗 -->
<ShopDealerWithdrawEdit 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 } 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 ShopDealerWithdrawEdit from './components/shopDealerWithdrawEdit.vue';
import { pageShopDealerWithdraw, removeShopDealerWithdraw, removeBatchShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw';
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from '@/api/shop/shopDealerWithdraw/model';
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {
ExclamationCircleOutlined,
CheckOutlined,
CloseOutlined,
DollarOutlined,
EditOutlined,
DeleteOutlined
} 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 ShopDealerWithdrawEdit from './components/shopDealerWithdrawEdit.vue';
import {
pageShopDealerWithdraw,
removeShopDealerWithdraw,
removeBatchShopDealerWithdraw,
updateShopDealerWithdraw
} from '@/api/shop/shopDealerWithdraw';
import type {ShopDealerWithdraw, ShopDealerWithdrawParam} from '@/api/shop/shopDealerWithdraw/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerWithdraw[]>([]);
// 当前编辑数据
const current = ref<ShopDealerWithdraw | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格选中数据
const selection = ref<ShopDealerWithdraw[]>([]);
// 当前编辑数据
const current = ref<ShopDealerWithdraw | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopDealerWithdraw({
...where,
...orders,
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 90,
fixed: 'left'
},
{
title: '提现金额',
dataIndex: 'money',
key: 'money',
align: 'center',
width: 150,
customRender: ({text}) => {
const amount = parseFloat(text || '0').toFixed(2);
return {
type: 'span',
children: `¥${amount}`
};
}
return pageShopDealerWithdraw({
...where,
...orders,
page,
limit
});
};
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo'
},
{
title: '收款信息',
dataIndex: 'paymentInfo',
key: 'paymentInfo'
},
// {
// title: '审核时间',
// dataIndex: 'auditTime',
// key: 'auditTime',
// align: 'center',
// width: 120,
// customRender: ({ text }) => text ? toDateString(new Date(text), 'yyyy-MM-dd HH:mm') : '-'
// },
// {
// title: '驳回原因',
// dataIndex: 'rejectReason',
// key: 'rejectReason',
// align: 'left',
// ellipsis: true,
// customRender: ({ text }) => text || '-'
// },
// {
// title: '来源平台',
// dataIndex: 'platform',
// key: 'platform',
// align: 'center',
// width: 100,
// customRender: ({ text }) => text || '-'
// },
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
},
{
title: '申请状态',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
width: 150
},
// {
// title: '驳回原因',
// dataIndex: 'rejectReason',
// key: 'rejectReason',
// align: 'center',
// },
// {
// title: '来源客户端',
// dataIndex: 'platform',
// key: 'platform',
// align: 'center',
// },
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 180,
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '分销商用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '提现金额',
dataIndex: 'money',
key: 'money',
align: 'center',
},
{
title: '打款方式 (10微信 20支付宝 30银行卡)',
dataIndex: 'payType',
key: 'payType',
align: 'center',
},
{
title: '支付宝姓名',
dataIndex: 'alipayName',
key: 'alipayName',
align: 'center',
},
{
title: '支付宝账号',
dataIndex: 'alipayAccount',
key: 'alipayAccount',
align: 'center',
},
{
title: '开户行名称',
dataIndex: 'bankName',
key: 'bankName',
align: 'center',
},
{
title: '银行开户名',
dataIndex: 'bankAccount',
key: 'bankAccount',
align: 'center',
},
{
title: '银行卡号',
dataIndex: 'bankCard',
key: 'bankCard',
align: 'center',
},
{
title: '申请状态 (10待审核 20审核通过 30驳回 40已打款)',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
},
{
title: '审核时间',
dataIndex: 'auditTime',
key: 'auditTime',
align: 'center',
},
{
title: '驳回原因',
dataIndex: 'rejectReason',
key: 'rejectReason',
align: 'center',
},
{
title: '来源客户端(APP、H5、小程序等)',
dataIndex: 'platform',
key: 'platform',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopDealerWithdrawParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 搜索 */
const reload = (where?: ShopDealerWithdrawParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerWithdraw) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerWithdraw) => {
const hide = message.loading('请求中..', 0);
removeShopDealerWithdraw(row.shopDealerWithdrawId)
.then((msg) => {
/* 审核通过 */
const approveWithdraw = (row: ShopDealerWithdraw) => {
Modal.confirm({
title: '审核通过确认',
content: `已核对信息进行核对,正确无误!`,
icon: createVNode(CheckOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在处理审核...', 0);
// 这里需要调用审核通过的API
setTimeout(() => {
hide();
message.success(msg);
updateShopDealerWithdraw({
id: row.id,
applyStatus: 20
});
message.success('审核通过成功');
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}, 1000);
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerWithdraw(selection.value.map((d) => d.shopDealerWithdrawId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
});
};
/* 审核驳回 */
const rejectWithdraw = (row: ShopDealerWithdraw) => {
let rejectReason = '';
Modal.confirm({
title: '审核驳回',
content: createVNode('div', null, [
createVNode('p', null, `用户ID: ${row.userId}`),
createVNode('p', null, `提现金额: ¥${parseFloat(row.money || '0').toFixed(2)}`),
createVNode('p', {style: 'margin-top: 12px;'}, '请输入驳回原因:'),
createVNode('textarea', {
placeholder: '请输入驳回原因...',
style: 'width: 100%; height: 80px; margin-top: 8px; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
onInput: (e: any) => {
rejectReason = e.target.value;
}
})
]),
icon: createVNode(CloseOutlined),
okText: '确认驳回',
okType: 'danger',
cancelText: '取消',
onOk: () => {
if (!rejectReason.trim()) {
message.error('请输入驳回原因');
return Promise.reject();
}
const hide = message.loading('正在处理审核...', 0);
setTimeout(() => {
hide();
message.success('审核驳回成功');
reload();
}, 1000);
}
});
};
/* 确认打款 */
const confirmPayment = (row: ShopDealerWithdraw) => {
Modal.confirm({
title: '确认打款',
content: `确定已向用户${row.bankAccount}完成打款?此操作不可撤销`,
icon: createVNode(DollarOutlined),
okText: '确认打款',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在确认打款...', 0);
setTimeout(() => {
updateShopDealerWithdraw({
id: row.id,
applyStatus: 40
})
hide();
message.success('打款确认成功');
reload();
}, 1000);
}
});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerWithdraw) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerWithdraw) => {
const hide = message.loading('请求中..', 0);
removeShopDealerWithdraw(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerWithdraw(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: ShopDealerWithdraw) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerWithdraw) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
query();
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerWithdraw'
};
export default {
name: 'ShopDealerWithdraw'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,220 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
: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="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="物流公司名称" name="expressName">
<a-input
allow-clear
placeholder="请输入物流公司名称"
v-model:value="form.expressName"
/>
</a-form-item>
<a-form-item label="物流公司编码 (微信)" name="wxCode">
<a-input
allow-clear
placeholder="请输入物流公司编码 (微信)"
v-model:value="form.wxCode"
/>
</a-form-item>
<a-form-item label="物流公司编码 (快递100)" name="kuaidi100Code">
<a-input
allow-clear
placeholder="请输入物流公司编码 (快递100)"
v-model:value="form.kuaidi100Code"
/>
</a-form-item>
<a-form-item label="物流公司编码 (快递鸟)" name="kdniaoCode">
<a-input
allow-clear
placeholder="请输入物流公司编码 (快递鸟)"
v-model:value="form.kdniaoCode"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="是否删除, 0否, 1是" name="deleted">
<a-input
allow-clear
placeholder="请输入是否删除, 0否, 1是"
v-model:value="form.deleted"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</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 { assignObject, uuid } from 'ele-admin-pro';
import { addShopExpress, updateShopExpress } from '@/api/shop/shopExpress';
import { ShopExpress } from '@/api/shop/shopExpress/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?: ShopExpress | 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<ShopExpress>({
expressId: undefined,
expressName: undefined,
wxCode: undefined,
kuaidi100Code: undefined,
kdniaoCode: undefined,
sortNumber: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopExpressId: undefined,
shopExpressName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopExpressName: [
{
required: true,
type: 'string',
message: '请填写物流公司名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
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 ? updateShopExpress : addShopExpress;
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) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,244 @@
<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="shopExpressId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopExpressEdit 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 } 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 ShopExpressEdit from './components/shopExpressEdit.vue';
import { pageShopExpress, removeShopExpress, removeBatchShopExpress } from '@/api/shop/shopExpress';
import type { ShopExpress, ShopExpressParam } from '@/api/shop/shopExpress/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopExpress[]>([]);
// 当前编辑数据
const current = ref<ShopExpress | 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 pageShopExpress({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '物流公司ID',
dataIndex: 'expressId',
key: 'expressId',
align: 'center',
width: 90,
},
{
title: '物流公司名称',
dataIndex: 'expressName',
key: 'expressName',
align: 'center',
},
{
title: '物流公司编码 (微信)',
dataIndex: 'wxCode',
key: 'wxCode',
align: 'center',
},
{
title: '物流公司编码 (快递100)',
dataIndex: 'kuaidi100Code',
key: 'kuaidi100Code',
align: 'center',
},
{
title: '物流公司编码 (快递鸟)',
dataIndex: 'kdniaoCode',
key: 'kdniaoCode',
align: 'center',
},
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopExpressParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopExpress) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopExpress) => {
const hide = message.loading('请求中..', 0);
removeShopExpress(row.shopExpressId)
.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);
removeBatchShopExpress(selection.value.map((d) => d.shopExpressId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopExpress) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopExpress'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -5,7 +5,7 @@
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑' : '添加'"
:title="isUpdate ? '编辑运费模板' : '添加运费模板'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -19,102 +19,75 @@
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="用户唯一小程序id" name="openId">
<a-input
allow-clear
placeholder="请输入用户唯一小程序id"
v-model:value="form.openId"
/>
</a-form-item>
<a-form-item label="小程序用户秘钥" name="sessionKey">
<a-input
allow-clear
placeholder="请输入小程序用户秘钥"
v-model:value="form.sessionKey"
/>
</a-form-item>
<a-form-item label="用户名" name="username">
<a-input
allow-clear
placeholder="请输入用户名"
v-model:value="form.username"
/>
</a-form-item>
<a-form-item label="头像地址" name="avatarUrl">
<a-input
allow-clear
placeholder="请输入头像地址"
v-model:value="form.avatarUrl"
/>
</a-form-item>
<a-form-item label="1男2女" name="gender">
<a-input
allow-clear
placeholder="请输入1男2女"
v-model:value="form.gender"
/>
</a-form-item>
<a-form-item label="国家" name="country">
<a-input
allow-clear
placeholder="请输入国家"
v-model:value="form.country"
/>
</a-form-item>
<a-form-item label="省份" name="province">
<a-input
allow-clear
placeholder="请输入省份"
v-model:value="form.province"
/>
</a-form-item>
<a-form-item label="城市" name="city">
<a-input
allow-clear
placeholder="请输入城市"
v-model:value="form.city"
/>
</a-form-item>
<a-form-item label="手机号码" name="phone">
<a-input
allow-clear
placeholder="请输入手机号码"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="积分" name="integral">
<a-input
allow-clear
placeholder="请输入积分"
v-model:value="form.integral"
/>
</a-form-item>
<a-form-item label="余额" name="money">
<a-input
allow-clear
placeholder="请输入余额"
v-model:value="form.money"
/>
</a-form-item>
<a-form-item label="" name="idcard">
<a-form-item label="" name="type">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.idcard"
v-model:value="form.type"
/>
</a-form-item>
<a-form-item label="" name="truename">
<a-form-item label="" name="title">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.truename"
v-model:value="form.title"
/>
</a-form-item>
<a-form-item label="是否管理员1是2否" name="isAdmin">
<a-form-item label="收件价格" name="firstAmount">
<a-input
allow-clear
placeholder="请输入是否管理员1是2否"
v-model:value="form.isAdmin"
placeholder="请输入收件价格"
v-model:value="form.firstAmount"
/>
</a-form-item>
<a-form-item label="续件价格" name="extraAmount">
<a-input
allow-clear
placeholder="请输入续件价格"
v-model:value="form.extraAmount"
/>
</a-form-item>
<a-form-item label="状态, 0已发布, 1待审核 2已驳回 3违规内容" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="是否删除, 0否, 1是" name="deleted">
<a-input
allow-clear
placeholder="请输入是否删除, 0否, 1是"
v-model:value="form.deleted"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
<a-form-item label="" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="首件数量/重量" name="firstNum">
<a-input
allow-clear
placeholder="请输入首件数量/重量"
v-model:value="form.firstNum"
/>
</a-form-item>
<a-form-item label="续件数量/重量" name="extraNum">
<a-input
allow-clear
placeholder="请输入续件数量/重量"
v-model:value="form.extraNum"
/>
</a-form-item>
</a-form>
@@ -125,8 +98,8 @@
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addUser, updateUser } from '@/api/system/user';
import { User } from '@/api/system/user/model';
import { addShopExpressTemplate, updateShopExpressTemplate } from '@/api/shop/shopExpressTemplate';
import { ShopExpressTemplate } from '@/api/shop/shopExpressTemplate/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
@@ -144,7 +117,7 @@
//
visible: boolean;
//
data?: User | null;
data?: ShopExpressTemplate | null;
}>();
const emit = defineEmits<{
@@ -161,26 +134,18 @@
const images = ref<ItemType[]>([]);
//
const form = reactive<User>({
const form = reactive<ShopExpressTemplate>({
id: undefined,
openId: undefined,
sessionKey: undefined,
username: undefined,
avatarUrl: undefined,
gender: undefined,
country: undefined,
province: undefined,
city: undefined,
phone: undefined,
integral: undefined,
money: undefined,
createTime: undefined,
idcard: undefined,
truename: undefined,
isAdmin: undefined,
type: undefined,
title: undefined,
firstAmount: undefined,
extraAmount: undefined,
deleted: undefined,
tenantId: undefined,
userId: undefined,
userName: '',
createTime: undefined,
updateTime: undefined,
firstNum: undefined,
extraNum: undefined,
status: 0,
comments: '',
sortNumber: 100
@@ -193,11 +158,11 @@
//
const rules = reactive({
userName: [
shopExpressTemplateName: [
{
required: true,
type: 'string',
message: '请填写名称',
message: '请填写运费模板名称',
trigger: 'blur'
}
]
@@ -231,7 +196,7 @@
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateUser : addUser;
const saveOrUpdate = isUpdate.value ? updateShopExpressTemplate : addShopExpressTemplate;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;

View File

@@ -0,0 +1,274 @@
<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="shopExpressTemplateId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopExpressTemplateEdit 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 } 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 ShopExpressTemplateEdit from './components/shopExpressTemplateEdit.vue';
import { pageShopExpressTemplate, removeShopExpressTemplate, removeBatchShopExpressTemplate } from '@/api/shop/shopExpressTemplate';
import type { ShopExpressTemplate, ShopExpressTemplateParam } from '@/api/shop/shopExpressTemplate/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopExpressTemplate[]>([]);
// 当前编辑数据
const current = ref<ShopExpressTemplate | 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 pageShopExpressTemplate({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'type',
key: 'type',
align: 'center',
},
{
title: '',
dataIndex: 'title',
key: 'title',
align: 'center',
},
{
title: '收件价格',
dataIndex: 'firstAmount',
key: 'firstAmount',
align: 'center',
},
{
title: '续件价格',
dataIndex: 'extraAmount',
key: 'extraAmount',
align: 'center',
},
{
title: '状态, 0已发布, 1待审核 2已驳回 3违规内容',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
},
{
title: '',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '首件数量/重量',
dataIndex: 'firstNum',
key: 'firstNum',
align: 'center',
},
{
title: '续件数量/重量',
dataIndex: 'extraNum',
key: 'extraNum',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopExpressTemplateParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopExpressTemplate) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopExpressTemplate) => {
const hide = message.loading('请求中..', 0);
removeShopExpressTemplate(row.shopExpressTemplateId)
.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);
removeBatchShopExpressTemplate(selection.value.map((d) => d.shopExpressTemplateId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopExpressTemplate) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopExpressTemplate'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,232 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
: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="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="" name="templateId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.templateId"
/>
</a-form-item>
<a-form-item label="0按件" name="type">
<a-input
allow-clear
placeholder="请输入0按件"
v-model:value="form.type"
/>
</a-form-item>
<a-form-item label="" name="provinceId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.provinceId"
/>
</a-form-item>
<a-form-item label="" name="cityId">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.cityId"
/>
</a-form-item>
<a-form-item label="首件数量/重量" name="firstNum">
<a-input
allow-clear
placeholder="请输入首件数量/重量"
v-model:value="form.firstNum"
/>
</a-form-item>
<a-form-item label="收件价格" name="firstAmount">
<a-input
allow-clear
placeholder="请输入收件价格"
v-model:value="form.firstAmount"
/>
</a-form-item>
<a-form-item label="续件价格" name="extraAmount">
<a-input
allow-clear
placeholder="请输入续件价格"
v-model:value="form.extraAmount"
/>
</a-form-item>
<a-form-item label="续件数量/重量" name="extraNum">
<a-input
allow-clear
placeholder="请输入续件数量/重量"
v-model:value="form.extraNum"
/>
</a-form-item>
<a-form-item label="状态, 0已发布, 1待审核 2已驳回 3违规内容" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="是否删除, 0否, 1是" name="deleted">
<a-input
allow-clear
placeholder="请输入是否删除, 0否, 1是"
v-model:value="form.deleted"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
<a-form-item label="" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</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 { assignObject, uuid } from 'ele-admin-pro';
import { addShopExpressTemplateDetail, updateShopExpressTemplateDetail } from '@/api/shop/shopExpressTemplateDetail';
import { ShopExpressTemplateDetail } from '@/api/shop/shopExpressTemplateDetail/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';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopExpressTemplateDetail | 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<ShopExpressTemplateDetail>({
id: undefined,
templateId: undefined,
type: undefined,
provinceId: undefined,
cityId: undefined,
firstNum: undefined,
firstAmount: undefined,
extraAmount: undefined,
extraNum: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopExpressTemplateDetailName: [
{
required: true,
type: 'string',
message: '请填写运费模板名称',
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 ? updateShopExpressTemplateDetail : addShopExpressTemplateDetail;
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) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,286 @@
<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="shopExpressTemplateDetailId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopExpressTemplateDetailEdit 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 } 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 ShopExpressTemplateDetailEdit from './components/shopExpressTemplateDetailEdit.vue';
import { pageShopExpressTemplateDetail, removeShopExpressTemplateDetail, removeBatchShopExpressTemplateDetail } from '@/api/shop/shopExpressTemplateDetail';
import type { ShopExpressTemplateDetail, ShopExpressTemplateDetailParam } from '@/api/shop/shopExpressTemplateDetail/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopExpressTemplateDetail[]>([]);
// 当前编辑数据
const current = ref<ShopExpressTemplateDetail | 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 pageShopExpressTemplateDetail({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '',
dataIndex: 'templateId',
key: 'templateId',
align: 'center',
},
{
title: '0按件',
dataIndex: 'type',
key: 'type',
align: 'center',
},
{
title: '',
dataIndex: 'provinceId',
key: 'provinceId',
align: 'center',
},
{
title: '',
dataIndex: 'cityId',
key: 'cityId',
align: 'center',
},
{
title: '首件数量/重量',
dataIndex: 'firstNum',
key: 'firstNum',
align: 'center',
},
{
title: '收件价格',
dataIndex: 'firstAmount',
key: 'firstAmount',
align: 'center',
},
{
title: '续件价格',
dataIndex: 'extraAmount',
key: 'extraAmount',
align: 'center',
},
{
title: '续件数量/重量',
dataIndex: 'extraNum',
key: 'extraNum',
align: 'center',
},
{
title: '状态, 0已发布, 1待审核 2已驳回 3违规内容',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center',
},
{
title: '',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopExpressTemplateDetailParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopExpressTemplateDetail) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopExpressTemplateDetail) => {
const hide = message.loading('请求中..', 0);
removeShopExpressTemplateDetail(row.shopExpressTemplateDetailId)
.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);
removeBatchShopExpressTemplateDetail(selection.value.map((d) => d.shopExpressTemplateDetailId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopExpressTemplateDetail) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopExpressTemplateDetail'
};
</script>
<style lang="less" scoped></style>

View File

@@ -19,122 +19,352 @@
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="名称" name="name">
<a-input allow-clear placeholder="请输入" v-model:value="form.name" />
<a-form-item label="礼品卡" name="name">
<a-input allow-clear placeholder="请输入礼品卡名称" v-model:value="form.name"/>
</a-form-item>
<a-form-item label="商品" name="goodsId">
<a-select v-model:value="form.goodsId">
<a-select-option
v-for="item in goodsList"
:key="item.goodsId"
:value="item.goodsId"
>
{{ item.name }}
<a-form-item label="关联商品" name="goodsId">
<a-select
v-model:value="form.goodsId"
placeholder="请选择关联商品"
show-search
:filter-option="false"
:loading="goodsLoading"
@search="searchGoods"
@change="onGoodsChange"
@dropdown-visible-change="onDropdownVisibleChange"
>
<a-select-option v-for="goods in goodsList" :key="goods.goodsId" :value="goods.goodsId">
<div class="goods-option">
<span>{{ goods.name }}</span>
<a-tag color="blue" style="margin-left: 8px;">¥{{ goods.price || 0 }}</a-tag>
</div>
</a-select-option>
<a-select-option v-if="goodsList.length === 0" disabled>
<div style="text-align: center; color: #999;">
{{ goodsLoading ? '加载中...' : '暂无商品数据' }}
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="数量" name="num">
<a-input-number v-model:value="form.num" :min="0" />
<a-form-item label="生成数量" name="num">
<a-input-number v-model:value="form.num" :min="0"/>
</a-form-item>
<a-form-item label="使用地址" name="useLocation">
<a-input
placeholder="请输入使用的门店地址"
v-model:value="form.useLocation"
/>
</a-form-item>
<a-form-item label="备注信息" name="comments">
<a-textarea
v-model:value="form.comments"
placeholder="请输入备注信息"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
</a-form>
<!-- 礼品卡预览 -->
<div class="gift-card-preview" v-if="form.name">
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">礼品卡预览</span>
</a-divider>
<div class="gift-card">
<div class="gift-card-header">
<div class="gift-card-title">{{ form.name }}</div>
<div class="gift-card-status">
<a-tag>
<span v-if="form.takeTime">领取时间{{ formatTime(form.takeTime) }}</span>
<span v-else>未领取</span>
</a-tag>
</div>
</div>
<div class="gift-card-body">
<div class="gift-card-code">
<span class="code-label text-gray-50">卡密</span>
<span class="code-value">{{ form.code || '自动生成' }}</span>
</div>
<div class="gift-card-goods" v-if="selectedGoods">
<span class="goods-label text-gray-50">关联商品</span>
<span class="goods-name">{{ selectedGoods.name }}</span>
<a-tag color="blue" style="margin-left: 8px;">¥{{ selectedGoods.price }}</a-tag>
</div>
<div class="gift-card-goods py-2" v-if="selectedGoods">
<span class="goods-label">使用地址</span>
<span class="goods-name">{{ form.useLocation }}</span>
</div>
</div>
<div class="gift-card-footer">
<div class="gift-card-info text-gray-50">
备注: {{ form.comments }}
</div>
</div>
</div>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { makeShopGift } from '@/api/shop/shopGift';
import { ShopGift } from '@/api/shop/shopGift/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { FormInstance } from 'ant-design-vue/es/form';
import { listShopGoods } from '@/api/shop/shopGoods';
import { ShopGoods } from '@/api/shop/shopGoods/model';
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {makeShopGift} from '@/api/shop/shopGift';
import {ShopGift} from '@/api/shop/shopGift/model';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {FormInstance} from 'ant-design-vue/es/form';
import {listShopGoods} from '@/api/shop/shopGoods';
import {ShopGoods} from '@/api/shop/shopGoods/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
}>();
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 maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 商品列表
const goodsList = ref<ShopGoods[]>([]);
// 商品加载状态
const goodsLoading = ref(false);
// 选中的商品
const selectedGoods = ref<ShopGoods | null>(null);
const rules = reactive({
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
goodsId: [{ required: true, message: '请选择商品', trigger: 'change' }],
num: [{ required: true, message: '请输入数量', trigger: 'blur' }]
});
const rules = reactive({
name: [{required: true, message: '请输入名称', trigger: 'blur'}],
goodsId: [{required: true, message: '请选择商品', trigger: 'change'}],
num: [{required: true, message: '请输入数量', trigger: 'blur'}]
});
// 用户信息
const form = reactive<ShopGift>({
id: undefined,
name: undefined,
code: undefined,
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
isShow: undefined,
status: undefined,
comments: undefined,
sortNumber: undefined,
userId: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
num: undefined
});
// 用户信息
const form = reactive<ShopGift>({
id: undefined,
name: undefined,
code: undefined,
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
isShow: undefined,
status: undefined,
useLocation: undefined,
comments: undefined,
sortNumber: undefined,
userId: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
num: 1000
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const goodsList = ref<ShopGoods[]>([]);
const getGoodsList = async () => {
goodsList.value = await listShopGoods();
};
getGoodsList();
const getGoodsList = async () => {
goodsList.value = await listShopGoods();
};
getGoodsList();
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
/* 搜索商品 */
const searchGoods = async (value: string) => {
if (value && value.trim()) {
goodsLoading.value = true;
try {
const res = await listShopGoods({keywords: value.trim()});
goodsList.value = res || [];
console.log('搜索到的商品:', goodsList.value);
} catch (e) {
console.error('搜索商品失败:', e);
goodsList.value = [];
} finally {
goodsLoading.value = false;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
makeShopGift(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, { immediate: true });
/* 下拉框显示状态改变 */
const onDropdownVisibleChange = (open: boolean) => {
if (open && goodsList.value.length === 0) {
// 当下拉框打开且没有数据时,加载默认商品列表
getGoodsList();
}
};
/* 商品选择改变 */
const onGoodsChange = (goodsId: number) => {
selectedGoods.value = goodsList.value.find(goods => goods.goodsId === goodsId) || null;
console.log('选中的商品:', selectedGoods.value);
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
makeShopGift(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, {immediate: true});
</script>
<style lang="less" scoped>
.goods-option,
.status-option {
display: flex;
align-items: center;
justify-content: space-between;
.ant-tag {
margin-left: 8px;
}
span {
color: #666;
font-size: 12px;
}
}
.gift-card-preview {
margin-top: 24px;
.gift-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
border-radius: 12px;
padding: 20px;
color: #333;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
&::before {
content: '';
position: absolute;
top: -50px;
right: -50px;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.gift-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.gift-card-title {
font-size: 20px;
font-weight: bold;
color: #f3f3f3;
}
}
.gift-card-body {
margin-bottom: 16px;
.gift-card-code {
margin-bottom: 12px;
.code-label {
font-weight: 600;
color: #f3f3f3;
}
.code-value {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 4px;
margin-left: 8px;
}
}
.gift-card-goods {
.goods-label {
font-weight: 600;
color: #f3f3f3;
}
.goods-name {
margin-left: 8px;
color: #f3f3f3;
}
}
}
.gift-card-footer {
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
.gift-card-info {
font-size: 12px;
color: #f3f3f3;
}
}
}
}
: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-select-selection-item) {
display: flex;
align-items: center;
}
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 600;
}
}
</style>

View File

@@ -1,75 +1,627 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<a-button type="primary" class="ele-btn-icon" @click="openMultiAdd">
<template #icon>
<PlusOutlined />
<PlusOutlined/>
</template>
<span>添加</span>
</a-button>
<a-button class="ele-btn-icon" @click="openMultiAdd">
<span>批量生成</span>
</a-button>
<a-button class="ele-btn-icon" @click="exportData">
<span>导出</span>
<a-input-search
allow-clear
v-model:value="where.keywords"
placeholder="名称|秘钥|用户ID"
style="width: 240px"
@search="reload"
@pressEnter="reload"
/>
<a-button
type="text"
:icon="h(QrcodeOutlined)"
@click="handleExport"
:loading="exportLoading"
>导出二维码
</a-button
>
<a-button
type="text"
@click="handlePrint">打印
</a-button>
<MakeCard v-model:visible="showMultiAdd" @done="done"></MakeCard>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import { watch, ref } from 'vue';
import {ShopGift, ShopGiftParam} from "@/api/shop/shopGift/model";
import MakeCard from "@/views/shop/shopGift/components/makeCard.vue";
import {exportShopGift} from "@/api/shop/shopGift";
import {message} from "ant-design-vue";
import {PlusOutlined, QrcodeOutlined} from '@ant-design/icons-vue';
import {watch, ref, h} from 'vue';
import {ShopGift, ShopGiftParam} from "@/api/shop/shopGift/model";
import MakeCard from "@/views/shop/shopGift/components/makeCard.vue";
import {listShopGift} from "@/api/shop/shopGift";
import {message} from "ant-design-vue";
import {
Document,
Packer,
Paragraph,
ImageRun,
AlignmentType,
Table,
TableRow,
TableCell,
WidthType
} from 'docx';
import {saveAs} from 'file-saver';
import QRCode from 'qrcode';
import useSearch from "@/utils/use-search";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: ShopGift[];
}>(),
{}
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: ShopGift[];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopGiftParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'done'): void;
}>();
// 表单数据
const {where} = useSearch<ShopGiftParam>({
keywords: ''
});
// 新增
// const add = () => {
// emit('add');
// };
const reload = () => {
emit('search', {...where});
};
const done = () => {
emit('done');
};
const showMultiAdd = ref(false)
const exportLoading = ref(false);
const openMultiAdd = () => {
showMultiAdd.value = true
};
// 批量导出二维码到Word文档
const handleExport = async () => {
try {
exportLoading.value = true;
message.loading('正在生成二维码文档,请稍候...', 0);
// 获取所有礼品卡数据
let giftList: ShopGift[] = [];
if (props.selection && props.selection.length > 0) {
// 如果有选中的数据,只导出选中的
giftList = props.selection;
} else {
// 如果没有选中,导出所有数据
giftList = await listShopGift();
}
if (!giftList || giftList.length === 0) {
message.error('没有礼品卡数据可导出');
return;
}
// 生成二维码图片
const qrCodeImages: { dataUrl: string; giftInfo: ShopGift }[] = [];
for (const gift of giftList) {
try {
// 生成二维码使用礼品卡code作为内容
const qrCodeDataUrl = await QRCode.toDataURL(String(gift.code), {
width: 200,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
});
qrCodeImages.push({dataUrl: qrCodeDataUrl, giftInfo: gift});
} catch (error) {
console.error(`生成礼品卡 ${gift.code} 的二维码失败:`, error);
}
}
if (qrCodeImages.length === 0) {
message.error('二维码生成失败');
return;
}
// 尝试创建Word文档如果失败则使用HTML方式
try {
await createWordDocument(qrCodeImages);
message.destroy();
message.success(`成功导出 ${qrCodeImages.length} 个礼品卡二维码`);
} catch (docError) {
console.warn('Word文档生成失败使用HTML方式:', docError);
createHtmlDocument(qrCodeImages);
message.destroy();
message.success(
`成功生成 ${qrCodeImages.length} 个礼品卡二维码HTML格式可直接打印`
);
}
} catch (error) {
console.error('导出失败:', error);
message.destroy();
message.error('导出失败,请重试');
} finally {
exportLoading.value = false;
}
};
// 创建Word文档
const createWordDocument = async (
qrCodeImages: { dataUrl: string; giftInfo: ShopGift }[]
) => {
const children: (Paragraph | Table)[] = [];
// 添加标题
children.push(
new Paragraph({
text: '礼品卡二维码清单',
alignment: AlignmentType.CENTER,
spacing: {after: 400}
})
);
const emit = defineEmits<{
(e: 'search', where?: ShopGiftParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'done'): void;
}>();
// 每行放置3个二维码保持适当间距
const itemsPerRow = 3;
const rows = Math.ceil(qrCodeImages.length / itemsPerRow);
// 新增
const add = () => {
emit('add');
};
for (let row = 0; row < rows; row++) {
const startIndex = row * itemsPerRow;
const endIndex = Math.min(startIndex + itemsPerRow, qrCodeImages.length);
const rowItems = qrCodeImages.slice(startIndex, endIndex);
const done = () => {
emit('done');
};
// 创建表格行来放置二维码
const qrCodeCells: TableCell[] = [];
const infoCells: TableCell[] = [];
const showMultiAdd = ref(false)
const openMultiAdd = () => {
showMultiAdd.value = true
};
for (let i = 0; i < itemsPerRow; i++) {
if (i < rowItems.length) {
const item = rowItems[i];
const exportData = async () => {
const hide = message.loading('请求中..', 0);
const ids = []
if (props.selection && props.selection.length) {
props.selection.forEach(d => {
ids.push(d.id)
})
// 将DataURL转换为Buffer
const base64Data = item.dataUrl.split(',')[1];
const binaryData = atob(base64Data);
const bytes = new Uint8Array(binaryData.length);
for (let j = 0; j < binaryData.length; j++) {
bytes[j] = binaryData.charCodeAt(j);
}
qrCodeCells.push(
new TableCell({
children: [
new Paragraph({
children: [
// @ts-ignore
new ImageRun({
data: bytes,
transformation: {
width: 150,
height: 150
}
})
],
alignment: AlignmentType.CENTER
})
],
width: {size: 33, type: WidthType.PERCENTAGE}
})
);
infoCells.push(
new TableCell({
children: [
new Paragraph({
text: `${item.giftInfo.code || '未设置'}`,
alignment: AlignmentType.CENTER
}),
new Paragraph({
text: `${item.giftInfo.name || ''}`,
alignment: AlignmentType.CENTER
})
],
width: {size: 33, type: WidthType.PERCENTAGE}
})
);
} else {
// 空单元格
qrCodeCells.push(
new TableCell({
children: [new Paragraph({text: ''})],
width: {size: 33, type: WidthType.PERCENTAGE}
})
);
infoCells.push(
new TableCell({
children: [new Paragraph({text: ''})],
width: {size: 33, type: WidthType.PERCENTAGE}
})
);
}
}
const res = await exportShopGift(ids);
window.open(res.url);
hide();
// 添加表格
children.push(
new Table({
rows: [
new TableRow({
children: qrCodeCells
}),
new TableRow({
children: infoCells
})
],
width: {size: 100, type: WidthType.PERCENTAGE}
})
);
// 添加行间距
children.push(
new Paragraph({
text: '',
spacing: {after: 400}
})
);
}
watch(
() => props.selection,
() => {}
);
// 创建文档
const doc = new Document({
sections: [
{
properties: {
page: {
size: {
orientation: 'portrait',
width: 11906, // A4宽度 (210mm)
height: 16838 // A4高度 (297mm)
},
margin: {
top: 1134, // 2cm
right: 1134, // 2cm
bottom: 1134, // 2cm
left: 1134 // 2cm
}
}
},
children
}
]
});
// 生成并下载文档
try {
const buffer = await Packer.toBlob(doc);
const fileName = `礼品卡二维码清单_${new Date()
.toISOString()
.slice(0, 10)}.docx`;
saveAs(buffer, fileName);
} catch (error) {
console.error('文档生成失败:', error);
// 如果Packer.toBlob失败尝试使用toBuffer
const buffer = await Packer.toBuffer(doc);
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
});
const fileName = `礼品卡二维码清单_${new Date()
.toISOString()
.slice(0, 10)}.docx`;
saveAs(blob, fileName);
}
};
// 创建HTML文档备用方案
const createHtmlDocument = (
qrCodeImages: { dataUrl: string; giftInfo: ShopGift }[]
) => {
const itemsPerRow = 3;
let htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>礼品卡二维码清单</title>
<style>
@page {
size: A4;
margin: 2cm;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.title {
text-align: center;
font-size: 24px;
font-weight: bold;
margin-bottom: 30px;
}
.qr-grid {
display: grid;
grid-template-columns: repeat(${itemsPerRow}, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.qr-item {
text-align: center;
page-break-inside: avoid;
}
.qr-code {
width: 150px;
height: 150px;
margin: 0 auto 10px;
}
.qr-info {
font-size: 12px;
line-height: 1.4;
}
@media print {
.no-print { display: none; }
}
</style>
</head>
<body>
<div class="title">礼品卡二维码清单</div>
<div class="qr-grid">
`;
qrCodeImages.forEach((item) => {
htmlContent += `
<div class="qr-item">
<img src="${item.dataUrl}" alt="QR Code" class="qr-code">
<div class="qr-info">
<div>礼品卡编号: ${item.giftInfo.code || '未设置'}</div>
<div>礼品卡名称: ${item.giftInfo.name || ''}</div>
<div>ID: ${item.giftInfo.id}</div>
</div>
</div>
`;
});
htmlContent += `
</div>
<div class="no-print" style="text-align: center; margin-top: 30px;">
<button onclick="window.print()" style="padding: 10px 20px; font-size: 16px;">打印文档</button>
<button onclick="window.close()" style="padding: 10px 20px; font-size: 16px; margin-left: 10px;">关闭</button>
</div>
</body>
</html>
`;
// 在新窗口中打开HTML文档
const newWindow = window.open('', '_blank');
if (newWindow) {
newWindow.document.write(htmlContent);
newWindow.document.close();
} else {
// 如果弹窗被阻止,创建下载链接
const blob = new Blob([htmlContent], {type: 'text/html;charset=utf-8'});
const fileName = `礼品卡二维码清单_${new Date()
.toISOString()
.slice(0, 10)}.html`;
saveAs(blob, fileName);
}
};
// 使用原生 window.print() 的打印功能
const handlePrint = async () => {
try {
message.loading('正在准备打印数据...', 0);
// 获取打印数据
let printData: ShopGift[] = [];
if (props.selection && props.selection.length > 0) {
printData = props.selection;
} else {
printData = await listShopGift();
}
if (!printData || printData.length === 0) {
message.destroy();
message.warning('没有数据可以打印');
return;
}
message.destroy();
// 创建打印窗口
const printWindow = window.open('', '_blank');
if (!printWindow) {
message.error('无法打开打印窗口,请检查浏览器弹窗设置');
return;
}
// 生成完整的HTML文档
const printHtml = createPrintHtml(printData);
// 写入HTML内容
printWindow.document.write(printHtml);
printWindow.document.close();
// 等待内容加载完成后打印
printWindow.onload = () => {
printWindow.print();
// 打印完成后关闭窗口
printWindow.onafterprint = () => {
printWindow.close();
};
};
} catch (error) {
message.destroy();
console.error('打印失败:', error);
message.error('打印失败,请重试');
}
};
// 创建完整的打印HTML文档
const createPrintHtml = (data: ShopGift[]) => {
const getStatusText = (record: ShopGift) => {
if (record.userId == 0) return '未领取';
if (record.userId > 0 && record.status === 0) return '已领取';
if (record.status === 1) return '已使用';
if (record.status === 2) return '已失效';
return '未知';
};
// 安全地处理数据,避免 undefined 或 null 值
const safeValue = (value: any) => {
if (value === null || value === undefined) return '';
return String(value).replace(/</g, '&lt;').replace(/>/g, '&gt;');
};
let tableRows = '';
data.forEach(record => {
tableRows += `
<tr>
<td>${safeValue(record.id)}</td>
<td>${safeValue(record.name)}</td>
<td>${safeValue(record.code)}</td>
<td>${safeValue(record.goodsName)}</td>
<td>${safeValue(getStatusText(record))}</td>
<td>${safeValue(record.createTime)}</td>
</tr>`;
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>礼品卡清单</title>
<style>
@page {
margin: 15mm;
size: A4;
}
body {
font-family: Arial, "Microsoft YaHei", sans-serif;
margin: 0;
padding: 20px;
font-size: 14px;
}
.header {
text-align: center;
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #333;
}
.info {
margin-bottom: 15px;
font-size: 12px;
color: #666;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
border: 1px solid #333;
padding: 8px;
text-align: center;
font-size: 12px;
}
th {
background-color: #f5f5f5;
font-weight: bold;
height: 35px;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.footer {
margin-top: 20px;
text-align: right;
font-size: 12px;
color: #666;
}
@media print {
.no-print {
display: none !important;
}
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
</head>
<body>
<div class="header">礼品卡清单</div>
<div class="info">
<div>打印时间:${new Date().toLocaleString()}</div>
<div>数据条数:${data.length} 条</div>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>秘钥</th>
<th>商品</th>
<th>状态</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
${tableRows}
</tbody>
</table>
<div class="footer">
<div>共 ${data.length} 条记录</div>
</div>
<div class="no-print" style="text-align: center; margin-top: 20px;">
<button onclick="window.print()" style="padding: 10px 20px; font-size: 14px;">重新打印</button>
<button onclick="window.close()" style="padding: 10px 20px; font-size: 14px; margin-left: 10px;">关闭</button>
</div>
</body>
</html>
`;
};
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -1,11 +1,11 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
width="65%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑礼品卡' : '添加礼品卡'"
:title="isUpdate ? '礼品卡详情' : '礼品卡详情'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -14,247 +14,694 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="" name="name">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="秘钥" name="code">
<a-input
allow-clear
placeholder="请输入秘钥"
v-model:value="form.code"
/>
</a-form-item>
<a-form-item label="商品ID" name="goodsId">
<a-input
allow-clear
placeholder="请输入商品ID"
v-model:value="form.goodsId"
/>
</a-form-item>
<a-form-item label="领取时间" name="takeTime">
<a-input
allow-clear
placeholder="请输入领取时间"
v-model:value="form.takeTime"
/>
</a-form-item>
<a-form-item label="操作人" name="operatorUserId">
<a-input
allow-clear
placeholder="请输入操作人"
v-model:value="form.operatorUserId"
/>
</a-form-item>
<a-form-item label="是否展示" name="isShow">
<a-input
allow-clear
placeholder="请输入是否展示"
v-model:value="form.isShow"
/>
</a-form-item>
<a-form-item label="状态, 0上架 1待上架 2待审核 3审核不通过" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="是否删除, 0否, 1是" name="deleted">
<a-input
allow-clear
placeholder="请输入是否删除, 0否, 1是"
v-model:value="form.deleted"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
<!-- 基本信息 -->
<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="name">
<a-input
placeholder="请输入礼品卡名称"
:disabled="isUpdate"
v-model:value="form.name"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="礼品卡密钥" name="code">
<a-input
placeholder="请输入礼品卡密钥"
v-model:value="form.code"
:disabled="isUpdate"
>
<template #suffix>
<a-button v-if="!isUpdate" type="link" size="small" @click="generateCode">
生成
</a-button>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="关联商品" name="goodsId">
<a-select
v-model:value="form.goodsId"
placeholder="请选择关联商品"
show-search
:filter-option="false"
:loading="goodsLoading"
@search="searchGoods"
:disabled="isUpdate"
@change="onGoodsChange"
@dropdown-visible-change="onDropdownVisibleChange"
>
<a-select-option v-for="goods in goodsList" :key="goods.goodsId" :value="goods.goodsId">
<div class="goods-option">
<span>{{ goods.name }}</span>
<a-tag color="blue" style="margin-left: 8px;">¥{{ goods.price || 0 }}</a-tag>
</div>
</a-select-option>
<a-select-option v-if="goodsList.length === 0" disabled>
<div style="text-align: center; color: #999;">
{{ goodsLoading ? '加载中...' : '暂无商品数据' }}
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12" v-if="!isUpdate">
<a-form-item label="生成数量" name="num">
<a-input-number
:min="1"
:max="1000"
placeholder="请输入生成数量"
v-model:value="form.num"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="使用地址" name="useLocation">
<a-input
placeholder="请输入使用的门店地址"
v-model:value="form.useLocation"
:disabled="isUpdate"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="备注信息" name="comments">
<a-textarea
v-model:value="form.comments"
:disabled="isUpdate"
placeholder="请输入备注信息"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
</a-col>
</a-row>
<!-- 状态设置 -->
<!-- <a-divider orientation="left">-->
<!-- <span style="color: #1890ff; font-weight: 600;">状态设置</span>-->
<!-- </a-divider>-->
<!-- <a-row :gutter="16">-->
<!-- <a-col :span="8">-->
<!-- <a-form-item label="上架状态" name="status">-->
<!-- <a-select v-model:value="form.status" placeholder="请选择上架状态">-->
<!-- <a-select-option :value="0">-->
<!-- <div class="status-option">-->
<!-- <a-tag color="success">已上架</a-tag>-->
<!-- <span>正常销售</span>-->
<!-- </div>-->
<!-- </a-select-option>-->
<!-- <a-select-option :value="1">-->
<!-- <div class="status-option">-->
<!-- <a-tag color="warning">待上架</a-tag>-->
<!-- <span>准备上架</span>-->
<!-- </div>-->
<!-- </a-select-option>-->
<!-- <a-select-option :value="2">-->
<!-- <div class="status-option">-->
<!-- <a-tag color="processing">待审核</a-tag>-->
<!-- <span>等待审核</span>-->
<!-- </div>-->
<!-- </a-select-option>-->
<!-- <a-select-option :value="3">-->
<!-- <div class="status-option">-->
<!-- <a-tag color="error">审核不通过</a-tag>-->
<!-- <span>审核失败</span>-->
<!-- </div>-->
<!-- </a-select-option>-->
<!-- </a-select>-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- <a-col :span="8">-->
<!-- <a-form-item label="展示状态" name="isShow">-->
<!-- <a-switch-->
<!-- v-model:checked="form.isShow"-->
<!-- checked-children="展示"-->
<!-- un-checked-children="隐藏"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- <a-col :span="8">-->
<!-- <a-form-item label="排序" name="sortNumber">-->
<!-- <a-input-number-->
<!-- :min="0"-->
<!-- placeholder="数字越小越靠前"-->
<!-- v-model:value="form.sortNumber"-->
<!-- style="width: 100%"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- </a-row>-->
<!-- 使用信息 -->
<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="takeTime">
<a-date-picker
v-model:value="form.takeTime"
placeholder="请选择领取时间"
:disabled="isUpdate"
show-time
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="领取用户ID" name="userId">
<a-input-number
:min="1"
placeholder="请输入领取用户ID"
v-model:value="form.userId"
:disabled="isUpdate"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="操作人ID" name="operatorUserId">
<a-input-number
:min="1"
placeholder="请输入操作人用户ID"
v-model:value="form.operatorUserId"
:disabled="isUpdate"
style="width: 300px"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="操作员备注" name="userId">
<a-textarea
v-model:value="form.operatorRemarks"
:disabled="isUpdate"
placeholder="请输入备注信息"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
</a-col>
</a-row>
<!-- 礼品卡预览 -->
<div class="gift-card-preview" v-if="form.name">
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">礼品卡预览</span>
</a-divider>
<div class="gift-card">
<div class="gift-card-header">
<div class="gift-card-title">{{ form.name }}</div>
<div class="gift-card-status">
<a-tag>
<span v-if="form.takeTime">领取时间{{ formatTime(form.takeTime) }}</span>
<span v-else>未领取</span>
</a-tag>
</div>
</div>
<div class="gift-card-body">
<div class="gift-card-code">
<span class="code-label text-gray-50">卡密</span>
<span class="code-value">{{ form.code || '未设置' }}</span>
</div>
<div class="gift-card-goods" v-if="selectedGoods">
<span class="goods-label text-gray-50">关联商品</span>
<span class="goods-name">{{ selectedGoods.name }}</span>
<a-tag color="blue" style="margin-left: 8px;">¥{{ selectedGoods.price }}</a-tag>
</div>
<div class="gift-card-goods py-2" v-if="selectedGoods">
<span class="goods-label">使用地址</span>
<span class="goods-name">{{ form.useLocation }}</span>
</div>
</div>
<div class="gift-card-footer">
<div class="gift-card-info text-gray-50">
备注: {{ form.comments }}
</div>
</div>
</div>
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addShopGift, updateShopGift } from '@/api/shop/shopGift';
import { ShopGift } from '@/api/shop/shopGift/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';
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject} from 'ele-admin-pro';
import {addShopGift, updateShopGift} from '@/api/shop/shopGift';
import {ShopGift} from '@/api/shop/shopGift/model';
import {FormInstance} from 'ant-design-vue/es/form';
import {listShopGoods} from '@/api/shop/shopGoods';
import {ShopGoods} from '@/api/shop/shopGoods/model';
import dayjs from 'dayjs';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopGift | null;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopGift | 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 maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<ShopGift>({
id: undefined,
name: undefined,
code: undefined,
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
isShow: undefined,
status: undefined,
comments: undefined,
sortNumber: undefined,
userId: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopGiftId: undefined,
shopGiftName: '',
status: 0,
comments: '',
sortNumber: 100
});
// 表单数据
const form = reactive<ShopGift>({
id: undefined,
name: '',
code: '',
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
operatorUserName: undefined,
operatorRemarks: undefined,
isShow: true,
status: 0,
useLocation: '',
comments: '',
sortNumber: 100,
userId: undefined,
deleted: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
num: 1000
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 商品列表
const goodsList = ref<ShopGoods[]>([]);
// 商品加载状态
const goodsLoading = ref(false);
// 选中的商品
const selectedGoods = ref<ShopGoods | null>(null);
// 表单验证规则
const rules = reactive({
shopGiftName: [
{
required: true,
type: 'string',
message: '请填写礼品卡名称',
trigger: 'blur'
}
]
});
/* 更新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.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
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 ? updateShopGift : addShopGift;
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) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
message: '请输入礼品卡名称',
trigger: 'blur'
},
{ immediate: true }
);
{
min: 2,
max: 50,
message: '礼品卡名称长度应在2-50个字符之间',
trigger: 'blur'
}
],
code: [
{
required: true,
message: '请输入礼品卡密钥',
trigger: 'blur'
},
{
min: 6,
max: 32,
message: '密钥长度应在6-32个字符之间',
trigger: 'blur'
}
],
goodsId: [
{
required: true,
message: '请选择关联商品',
trigger: 'change'
}
],
num: [
{
required: true,
message: '请输入生成数量',
trigger: 'blur'
},
{
validator: (rule: any, value: any) => {
if (value && (value < 1 || value > 1000)) {
return Promise.reject('生成数量必须在1-1000之间');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
status: [
{
required: true,
message: '请选择上架状态',
trigger: 'change'
}
]
});
/* 生成密钥 */
const generateCode = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
form.code = result;
};
/* 搜索商品 */
const searchGoods = async (value: string) => {
if (value && value.trim()) {
goodsLoading.value = true;
try {
const res = await listShopGoods({keywords: value.trim()});
goodsList.value = res || [];
console.log('搜索到的商品:', goodsList.value);
} catch (e) {
console.error('搜索商品失败:', e);
goodsList.value = [];
} finally {
goodsLoading.value = false;
}
}
};
/* 下拉框显示状态改变 */
const onDropdownVisibleChange = (open: boolean) => {
if (open && goodsList.value.length === 0) {
// 当下拉框打开且没有数据时,加载默认商品列表
getGoodsList();
}
};
/* 商品选择改变 */
const onGoodsChange = (goodsId: number) => {
selectedGoods.value = goodsList.value.find(goods => goods.goodsId === goodsId) || null;
console.log('选中的商品:', selectedGoods.value);
};
/* 获取状态颜色 */
const getStatusColor = () => {
const colorMap = {
0: 'success',
1: 'warning',
2: 'processing',
3: 'error'
};
return colorMap[form.status] || 'default';
};
/* 获取状态文本 */
const getStatusText = () => {
const textMap = {
0: '已上架',
1: '待上架',
2: '待审核',
3: '审核不通过'
};
return textMap[form.status] || '未知状态';
};
/* 格式化时间 */
const formatTime = (time: any) => {
if (!time) return '';
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
};
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
// 处理时间字段转换
if (formData.takeTime && dayjs.isDayjs(formData.takeTime)) {
formData.takeTime = formData.takeTime.format('YYYY-MM-DD HH:mm:ss');
}
// 处理数据类型转换
if (formData.isShow !== undefined) {
formData.isShow = formData.isShow === '1' || formData.isShow === true;
}
console.log('提交的礼品卡数据:', formData);
const saveOrUpdate = isUpdate.value ? updateShopGift : addShopGift;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
console.error('保存失败:', e);
});
})
.catch((errors) => {
console.error('表单验证失败:', errors);
});
};
/* 获取商品列表 */
const getGoodsList = async () => {
if (goodsLoading.value) return; // 防止重复加载
goodsLoading.value = true;
try {
const res = await listShopGoods({pageSize: 50}); // 限制返回数量
goodsList.value = res || [];
console.log('获取到的商品列表:', goodsList.value);
} catch (e) {
console.error('获取商品列表失败:', e);
goodsList.value = [];
} finally {
goodsLoading.value = false;
}
};
watch(
() => props.visible,
async (visible) => {
if (visible) {
await getGoodsList();
if (props.data) {
assignObject(form, props.data);
// 处理时间字段转换
if (props.data.takeTime) {
form.takeTime = dayjs(props.data.takeTime);
}
// 设置选中的商品
if (props.data.goodsId) {
selectedGoods.value = goodsList.value.find(goods => goods.goodsId === props.data.goodsId) || null;
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
id: undefined,
name: '',
code: '',
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
isShow: true,
status: 0,
comments: '',
sortNumber: 100,
userId: undefined,
deleted: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
num: 1000
});
selectedGoods.value = null;
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>
<style lang="less" scoped>
.goods-option,
.status-option {
display: flex;
align-items: center;
justify-content: space-between;
.ant-tag {
margin-left: 8px;
}
span {
color: #666;
font-size: 12px;
}
}
.gift-card-preview {
margin-top: 24px;
.gift-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
border-radius: 12px;
padding: 20px;
color: #333;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
&::before {
content: '';
position: absolute;
top: -50px;
right: -50px;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.gift-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.gift-card-title {
font-size: 20px;
font-weight: bold;
color: #f3f3f3;
}
}
.gift-card-body {
margin-bottom: 16px;
.gift-card-code {
margin-bottom: 12px;
.code-label {
font-weight: 600;
color: #f3f3f3;
}
.code-value {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 4px;
margin-left: 8px;
}
}
.gift-card-goods {
.goods-label {
font-weight: 600;
color: #f3f3f3;
}
.goods-name {
margin-left: 8px;
color: #f3f3f3;
}
}
}
.gift-card-footer {
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
.gift-card-info {
font-size: 12px;
color: #f3f3f3;
}
}
}
}
: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-select-selection-item) {
display: flex;
align-items: center;
}
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 600;
}
}
</style>

View File

@@ -22,12 +22,26 @@
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'goodsId'">
<div>{{ record.goodsName }}</div>
<div class="text-gray-300" v-if="record.nickName">领取人{{ record.userId }}</div>
<div class="text-gray-300" v-if="record.operatorUserId">核销人{{ record.operatorUserId }}</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
<a-space>
<a-tag v-if="record.userId == 0">未领取</a-tag>
<a-tag v-if="record.userId > 0 && record.status === 0" color="green">已领取</a-tag>
<a-tag v-if="record.status === 1" color="green">已使用</a-tag>
<a-tag v-if="record.status === 2" color="red">已失效</a-tag>
</a-space>
</template>
<template v-if="column.key === 'createTime'">
<div v-if="record.createTime">创建时间{{ record.createTime }}</div>
<div v-if="record.takeTime" class="text-green-500">领取时间{{ record.takeTime }}</div>
<div v-if="record.verificationTime" class="text-purple-500">核销时间{{ record.verificationTime }}</div>
</template>
<template v-if="column.key === 'action'">
<a-space>
@@ -55,7 +69,6 @@
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
@@ -127,19 +140,18 @@
align: 'center',
},
{
title: '领取时间',
dataIndex: 'takeTime',
key: 'takeTime',
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 170
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
sorter: true
},
{
title: '操作',
@@ -228,6 +240,7 @@
}
};
};
query();
</script>

View File

@@ -77,9 +77,17 @@
/>
</a-space>
</a-form-item>
<a-form-item label="团长价" name="originPrice">
<a-form-item label="市场价" name="salePrice">
<a-input-number
:placeholder="`团长价`"
:placeholder="`市场价`"
style="width: 240px"
:min="0.01"
v-model:value="form.salePrice"
/>
</a-form-item>
<a-form-item label="会员价" name="originPrice">
<a-input-number
:placeholder="`会员价`"
style="width: 240px"
:min="0.01"
v-model:value="form.dealerPrice"

View File

@@ -198,7 +198,7 @@ const columns = ref<ColumnItem[]>([
sorter: true,
ellipsis: true,
width: 180,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd')
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {
// title: '操作',

View File

@@ -83,7 +83,6 @@
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);
@@ -117,15 +116,11 @@
id: undefined,
goodsId: undefined,
issueCouponId: undefined,
sortNumber: undefined,
status: undefined,
deleted: undefined,
userId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopGoodsCouponId: undefined,
shopGoodsCouponName: '',
status: 0,
comments: '',
sortNumber: 100
@@ -148,20 +143,6 @@
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
@@ -199,13 +180,6 @@
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;

View File

@@ -182,12 +182,7 @@
status: undefined,
comments: undefined,
tenantId: undefined,
createTime: undefined,
shopGoodsSkuId: undefined,
shopGoodsSkuName: '',
status: 0,
comments: '',
sortNumber: 100
createTime: undefined
});
/* 更新visible */

View File

@@ -875,8 +875,8 @@ const isRefundStatus = (orderStatus?: number) => {
// 判断是否可以删除订单
const canDeleteOrder = (order: ShopOrder) => {
// 已完成、已取消、退款成功的订单可以删除
return [1, 2, 6].includes(order.orderStatus || 0);
// 已完成、已取消、退款成功的订单可以删除 (原来是[1, 2, 6],后面改成只有取消的订单能删除)
return [2].includes(order.orderStatus || 0);
};
// 判断是否可以申请退款

View File

@@ -13,6 +13,14 @@
<!-- </template>-->
<!-- <span>批量删除</span>-->
<!-- </a-button>-->
<a-input-search
allow-clear
v-model:value="where.orderNo"
placeholder="订单编号"
style="width: 240px"
@search="reload"
@pressEnter="reload"
/>
<a-select
v-model:value="where.type"
style="width: 150px"
@@ -64,11 +72,30 @@
/>
<a-input-search
allow-clear
placeholder="请输入关键词"
style="width: 280px"
:placeholder="getSearchPlaceholder()"
style="width: 320px"
v-model:value="where.keywords"
@search="reload"
/>
>
<template #addonBefore>
<a-select
v-model:value="type"
style="width: 88px;"
@change="onType"
>
<a-select-option value="">不限</a-select-option>
<a-select-option value="userId">
用户ID
</a-select-option>
<a-select-option value="phone">
手机号
</a-select-option>
<a-select-option value="nickname">
昵称
</a-select-option>
</a-select>
</template>
</a-input-search>
<a-button @click="reset">重置</a-button>
<a-button @click="handleExport">导出</a-button>
</a-space>
@@ -106,6 +133,8 @@
createTimeStart: undefined,
createTimeEnd: undefined,
userId: undefined,
payUserId: undefined,
nickname: undefined,
phone: undefined,
payStatus: undefined,
orderStatus: undefined,
@@ -113,14 +142,35 @@
});
const reload = () => {
emit('search', where);
emit('search', {...where, keywords: type.value == '' ? where.keywords : undefined});
};
// 批量删除
const removeBatch = () => {
emit('remove');
// const removeBatch = () => {
// emit('remove');
// };
const onType = () => {
resetFields();
};
// 获取搜索框placeholder
const getSearchPlaceholder = () => {
switch (type.value){
case 'userId':
where.userId = Number(where.keywords);
return '请输入用户ID';
case 'phone':
where.phone = where.keywords;
return '请输入手机号';
case 'nickname':
where.nickname = where.keywords;
return '请输入用户昵称';
default:
return '请输入搜索内容';
}
}
/* 搜索 */
const search = () => {
const [d1, d2] = dateRange.value ?? [];
@@ -143,6 +193,7 @@
const loading = ref(false);
const orders = ref<ShopOrder[]>([])
const xlsFileName = ref<string>();
const type = ref('');
// 导出
const handleExport = async () => {

View File

@@ -16,8 +16,8 @@
<a-tab-pane key="undelivered" tab="待发货"/>
<a-tab-pane key="unreceived" tab="待收货"/>
<a-tab-pane key="completed" tab="已完成"/>
<a-tab-pane key="refunded" tab="已退款"/>
<a-tab-pane key="cancelled" tab="已取消"/>
<a-tab-pane key="refunded" tab="退货/售后"/>
<a-tab-pane key="cancelled" tab="已关闭"/>
</a-tabs>
<ele-pro-table
ref="tableRef"
@@ -83,8 +83,8 @@
<template v-if="column.key === 'orderStatus'">
<a-tag v-if="record.orderStatus === 0">未完成</a-tag>
<a-tag v-if="record.orderStatus === 1" color="green">已完成</a-tag>
<a-tag v-if="record.orderStatus === 2" color="red">取消</a-tag>
<a-tag v-if="record.orderStatus === 3" color="red">取消</a-tag>
<a-tag v-if="record.orderStatus === 2">关闭</a-tag>
<a-tag v-if="record.orderStatus === 3" color="red">关闭</a-tag>
<a-tag v-if="record.orderStatus === 4" color="red">退款申请中</a-tag>
<a-tag v-if="record.orderStatus === 5" color="red">退款被拒绝</a-tag>
<a-tag v-if="record.orderStatus === 6" color="orange">退款成功</a-tag>
@@ -113,11 +113,11 @@
</a>
<a-divider type="vertical"/>
<a
@click.stop="openEdit(record)"
@click.stop="handleCancelOrder(record)"
>
<a class="ele-text-warning">
<CloseOutlined /> 取消
</a>
<span class="ele-text-warning">
<CloseOutlined /> 关闭
</span>
</a>
</template>
@@ -174,14 +174,14 @@
</a>
</template>
<!-- 删除操作 - 已完成取消退款成功的订单可以删除 -->
<!-- 删除操作 - 已完成关闭退款成功的订单可以删除 -->
<template v-if="canDeleteOrder(record)">
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此订单吗?删除后无法恢复。"
@confirm.stop="remove(record)"
@confirm="remove(record)"
>
<a class="ele-text-danger">
<a class="ele-text-danger" @click.stop>
<DeleteOutlined /> 删除
</a>
</a-popconfirm>
@@ -258,6 +258,7 @@ const datasource: DatasourceFunction = ({
if (filters) {
where.status = filters.status;
}
where.type = 0;
return pageShopOrder({
...where,
...orders,
@@ -316,7 +317,7 @@ const columns = ref<ColumnItem[]>([
title: '订单状态',
dataIndex: 'orderStatus',
key: 'orderStatus',
align: 'center',
align: 'center'
},
// {
// title: '备注',
@@ -395,11 +396,11 @@ const onTabs = () => {
filterParams.statusFilter = 5;
break;
case 'cancelled':
// 已取消order_status = 2
// 已关闭order_status = 2
filterParams.statusFilter = 8;
break;
case 'refunded':
// 退款order_status = 6
// 退款/售后order_status = 6
filterParams.statusFilter = 6;
break;
case 'deleted':
@@ -452,7 +453,7 @@ const query = () => {
};
/* 辅助判断函数 */
// 判断是否为取消状态
// 判断是否为关闭状态
const isCancelledStatus = (orderStatus?: number) => {
return [2, 3].includes(orderStatus || 0);
};
@@ -464,8 +465,8 @@ const isRefundStatus = (orderStatus?: number) => {
// 判断是否可以删除订单
const canDeleteOrder = (order: ShopOrder) => {
// 已完成、已取消、退款成功的订单可以删除
return [1, 2, 6].includes(order.orderStatus || 0);
// 已完成、已关闭、退款成功的订单可以删除 (原来是[1, 2, 6],后面改成只有关闭的订单能删除)
return [2].includes(order.orderStatus || 0);
};
/* 订单操作方法 */
@@ -475,21 +476,21 @@ const handleEditOrder = (record: ShopOrder) => {
// TODO: 实现订单修改功能
};
// 取消订单
// 关闭订单
const handleCancelOrder = (record: ShopOrder) => {
Modal.confirm({
title: '确认取消订单',
content: '确定要取消此订单吗?取消后无法恢复。',
title: '确认关闭订单',
content: '确定要关闭此订单吗?关闭后无法恢复。',
onOk: async () => {
try {
await updateShopOrder({
...record,
orderStatus: 2 // 已取消
orderStatus: 2 // 已关闭
});
message.success('订单已取消');
message.success('订单已关闭');
reload();
} catch (error: any) {
message.error(error.message || '取消订单失败');
message.error(error.message || '关闭订单失败');
}
}
});

View File

@@ -1,396 +0,0 @@
import {Avatar, Cell, Space, Tabs, Button, TabPane, Image} from '@nutui/nutui-react-taro'
import {useEffect, useState, CSSProperties} from "react";
import {View} from '@tarojs/components'
import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro'
import dayjs from "dayjs";
import {pageShopOrder, removeShopOrder, updateShopOrder} from "@/api/shop/shopOrder";
import {ShopOrder, ShopOrderParam} from "@/api/shop/shopOrder/model";
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import {copyText} from "@/utils/common";
const getInfiniteUlStyle = (showSearch: boolean = false): CSSProperties => ({
marginTop: showSearch ? '65px' : '44px', // 如果显示搜索框,增加更多的上边距
height: showSearch ? '75vh' : '82vh', // 相应调整高度
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
})
// 统一的订单状态标签配置,与后端 statusFilter 保持一致
const tabs = [
{
index: 0,
key: '全部',
title: '全部',
description: '所有订单',
statusFilter: undefined // 不传statusFilter显示所有订单
},
{
index: 1,
key: '待付款',
title: '待付款',
description: '等待付款的订单',
statusFilter: 0 // 对应后端pay_status = false
},
{
index: 2,
key: '待发货',
title: '待发货',
description: '已付款待发货的订单',
statusFilter: 1 // 对应后端pay_status = true AND delivery_status = 10
},
{
index: 3,
key: '待收货',
title: '待收货',
description: '已发货待收货的订单',
statusFilter: 3 // 对应后端pay_status = true AND delivery_status = 20
},
{
index: 4,
key: '已完成',
title: '已完成',
description: '已完成的订单',
statusFilter: 5 // 对应后端order_status = 1
},
{
index: 5,
key: '已取消',
title: '已取消',
description: '已取消/退款的订单',
statusFilter: 6 // 对应后端order_status = 6 (已退款)
}
]
// 扩展订单接口,包含商品信息
interface OrderWithGoods extends ShopOrder {
orderGoods?: ShopOrderGoods[];
}
interface OrderListProps {
data: ShopOrder[];
onReload?: () => void;
searchParams?: ShopOrderParam;
showSearch?: boolean;
}
function OrderList(props: OrderListProps) {
const [list, setList] = useState<OrderWithGoods[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [tapIndex, setTapIndex] = useState<string | number>(0)
const [loading, setLoading] = useState(false)
// 获取订单状态文本
const getOrderStatusText = (order: ShopOrder) => {
console.log(order,'order')
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
if (order.orderStatus === 4) return '退款申请中';
if (order.orderStatus === 5) return '退款被拒绝';
if (order.orderStatus === 6) return '退款成功';
if (order.orderStatus === 7) return '客户端申请退款';
// 检查支付状态 (payStatus为boolean类型false/0表示未付款true/1表示已付款)
if (!order.payStatus) return '等待买家付款';
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) return '待收货';
if (order.deliveryStatus === 30) return '已收货';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
if (order.orderStatus === 0) return '未使用';
return '未知状态';
};
// 获取订单状态颜色
const getOrderStatusColor = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return 'text-gray-500'; // 已取消
if (order.orderStatus === 4) return 'text-orange-500'; // 退款申请中
if (order.orderStatus === 5) return 'text-red-500'; // 退款被拒绝
if (order.orderStatus === 6) return 'text-green-500'; // 退款成功
if (order.orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
// 检查支付状态
if (!order.payStatus) return 'text-orange-500'; // 等待买家付款
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货
if (order.deliveryStatus === 20) return 'text-purple-500'; // 待收货
if (order.deliveryStatus === 30) return 'text-green-500'; // 已收货
// 最后检查订单完成状态
if (order.orderStatus === 1) return 'text-green-600'; // 已完成
if (order.orderStatus === 0) return 'text-gray-500'; // 未使用
return 'text-gray-600'; // 默认颜色
};
// 使用后端统一的 statusFilter 进行筛选
const getOrderStatusParams = (index: string | number) => {
let params: ShopOrderParam = {};
// 添加用户ID过滤
params.userId = Taro.getStorageSync('UserId');
// 获取当前tab的statusFilter配置
const currentTab = tabs.find(tab => tab.index === Number(index));
if (currentTab && currentTab.statusFilter !== undefined) {
params.statusFilter = currentTab.statusFilter;
}
console.log(`Tab ${index} (${currentTab?.title}) 筛选参数:`, params);
return params;
};
const reload = async (resetPage = false) => {
setLoading(true);
const currentPage = resetPage ? 1 : page;
const statusParams = getOrderStatusParams(tapIndex);
const searchConditions = {
page: currentPage,
...statusParams,
...props.searchParams
};
console.log('订单筛选条件:', {
tapIndex,
statusParams,
searchConditions
});
try {
const res = await pageShopOrder(searchConditions);
let newList: OrderWithGoods[] = [];
if (res?.list && res?.list.length > 0) {
// 为每个订单获取商品信息
const ordersWithGoods = await Promise.all(
res.list.map(async (order) => {
try {
const orderGoods = await listShopOrderGoods({ orderId: order.orderId });
return {
...order,
orderGoods: orderGoods || []
};
} catch (error) {
console.error('获取订单商品失败:', error);
return {
...order,
orderGoods: []
};
}
})
);
// 合并数据
newList = resetPage ? ordersWithGoods : list?.concat(ordersWithGoods);
setHasMore(true);
} else {
newList = [];
setHasMore(false);
}
setList(newList || []);
setPage(currentPage);
setLoading(false);
} catch (error) {
console.error('加载订单失败:', error);
setLoading(false);
}
};
const reloadMore = async () => {
setPage(page + 1);
reload();
};
// 格式化日期为后端期望的格式
const formatDateForBackend = (date: Date) => {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
};
// 确认收货
const confirmReceive = async (order: ShopOrder) => {
try {
await updateShopOrder({
...order,
deliveryStatus: 30, // 已收货
orderStatus: 1 // 已完成
});
Taro.showToast({
title: '确认收货成功',
});
reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
} catch (error) {
Taro.showToast({
title: '确认收货失败',
});
}
};
// 取消订单
const cancelOrder = async (order: ShopOrder) => {
try {
await removeShopOrder(order.orderId);
Taro.showToast({
title: '订单已删除',
});
reload(true); // 重新加载列表
props.onReload?.(); // 通知父组件刷新
} catch (error) {
console.error('取消订单失败:', error);
Taro.showToast({
title: '取消订单失败',
});
}
};
useEffect(() => {
reload(true); // 首次加载或tab切换时重置页码
}, [tapIndex]); // 监听tapIndex变化
useEffect(() => {
reload(true); // 搜索参数变化时重置页码
}, [props.searchParams]); // 监听搜索参数变化
return (
<>
<Tabs
align={'left'}
className={'fixed left-0'}
style={{
top: '44px',
zIndex: 998,
borderBottom: '1px solid #e5e5e5'
}}
tabStyle={{
backgroundColor: '#ffffff',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
value={tapIndex}
onChange={(paneKey) => {
console.log('Tab切换到:', paneKey, '对应状态:', tabs[paneKey]?.title);
setTapIndex(paneKey)
}}
>
{
tabs?.map((item, index) => {
return (
<TabPane
key={index}
title={loading && tapIndex === index ? `${item.title}...` : item.title}
></TabPane>
)
})
}
</Tabs>
<div style={getInfiniteUlStyle(props.showSearch)} id="scroll">
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
}}
onScrollToUpper={() => {
}}
loadingText={
<>
</>
}
loadMoreText={
<>
</>
}
>
{list?.map((item, index) => {
return (
<Cell key={index} style={{padding: '16px'}} onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
<Space direction={'vertical'} className={'w-full flex flex-col'}>
<View className={'order-no flex justify-between'}>
<View className={'text-gray-600 font-bold text-sm'}
onClick={(e) => {e.stopPropagation(); copyText(`${item.orderNo}`)}}>{item.orderNo}</View>
<View className={`${getOrderStatusColor(item)} font-medium`}>{getOrderStatusText(item)}</View>
</View>
<div
className={'create-time text-gray-400 text-xs'}>{dayjs(item.createTime).format('YYYY年MM月DD日 HH:mm:ss')}</div>
{/* 商品信息 */}
<div className={'goods-info'}>
{item.orderGoods && item.orderGoods.length > 0 ? (
item.orderGoods.map((goods, goodsIndex) => (
<div key={goodsIndex} className={'flex items-center mb-2'}>
<Image
src={goods.image || '/default-goods.png'}
width="50"
height="50"
lazyLoad={false}
className={'rounded'}
/>
<div className={'ml-2 flex-1'}>
<div className={'text-sm font-bold'}>{goods.goodsName}</div>
{goods.spec && <div className={'text-gray-500 text-xs'}>{goods.spec}</div>}
<div className={'text-gray-500 text-xs'}>{goods.totalNum}</div>
</div>
<div className={'text-sm'}>{goods.price}</div>
</div>
))
) : (
<div className={'flex items-center'}>
<Avatar
src='/default-goods.png'
size={'50'}
shape={'square'}
/>
<div className={'ml-2'}>
<div className={'text-sm'}>{item.title || '订单商品'}</div>
<div className={'text-gray-400 text-xs'}>{item.totalNum}</div>
</div>
</div>
)}
</div>
<div className={'w-full text-right'}>{item.payPrice}</div>
{/* 操作按钮 */}
<Space className={'btn flex justify-end'}>
{/* 待付款状态:显示取消订单和立即支付 */}
{(!item.payStatus) && item.orderStatus !== 2 && (
<Space>
<Button size={'small'} onClick={(e) => {e.stopPropagation(); cancelOrder(item)}}></Button>
<Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); console.log('立即支付')}}></Button>
</Space>
)}
{/* 待收货状态:显示确认收货 */}
{item.deliveryStatus === 20 && (
<Button size={'small'} type="primary" onClick={(e) => {e.stopPropagation(); confirmReceive(item)}}></Button>
)}
{/* 已完成状态:显示申请退款 */}
{item.orderStatus === 1 && (
<Button size={'small'} onClick={(e) => {e.stopPropagation(); console.log('申请退款')}}>退</Button>
)}
{/* 退款相关状态的按钮可以在这里添加 */}
</Space>
</Space>
</Cell>
)
})}
</InfiniteLoading>
</div>
</>
)
}
export default OrderList

View File

@@ -263,12 +263,7 @@
userId: undefined,
tenantId: undefined,
updateTime: undefined,
createTime: undefined,
shopOrderGoodsId: undefined,
shopOrderGoodsName: '',
status: 0,
comments: '',
sortNumber: 100
createTime: undefined
});
/* 更新visible */

View File

@@ -132,12 +132,7 @@
status: undefined,
sortNumber: undefined,
tenantId: undefined,
createTime: undefined,
shopSpecId: undefined,
shopSpecName: '',
status: 0,
comments: '',
sortNumber: 100
createTime: undefined
});
/* 更新visible */

View File

@@ -99,13 +99,8 @@
specId: undefined,
specValue: undefined,
comments: undefined,
sortNumber: undefined,
tenantId: undefined,
createTime: undefined,
shopSpecValueId: undefined,
shopSpecValueName: '',
status: 0,
comments: '',
sortNumber: 100
});

View File

@@ -0,0 +1,39 @@
<!-- 机构选择下拉框 -->
<template>
<a-tree-select
allow-clear
tree-default-expand-all
:placeholder="placeholder"
:value="value || undefined"
:tree-data="data"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="updateValue"
/>
</template>
<script lang="ts" setup>
import type { Organization } from '@/api/system/organization/model';
const emit = defineEmits<{
(e: 'update:value', value?: number): void;
}>();
withDefaults(
defineProps<{
// 选中的数据(v-modal)
value?: number;
// 提示信息
placeholder?: string;
// 机构数据
data: Organization[];
}>(),
{
placeholder: '请选择角色'
}
);
/* 更新选中数据 */
const updateValue = (value?: number) => {
emit('update:value', value);
};
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,278 @@
<!-- 管理员编辑弹窗 -->
<template>
<ele-modal
:width="500"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '编辑客户' : '添加客户'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 17, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input
allow-clear
:maxlength="11"
:disabled="isUpdate"
placeholder="请输入手机号"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入邮箱"
v-model:value="form.email"
/>
</a-form-item>
<a-form-item label="所属机构" name="type">
<org-select
:data="organizationList"
placeholder="请选择所属机构"
v-model:value="form.organizationId"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { emailReg, phoneReg } from 'ele-admin-pro/es';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { addUser, updateUser, checkExistence } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
import OrgSelect from './org-select.vue';
import { Organization } from '@/api/system/organization/model';
import { Grade } from '@/api/user/grade/model';
import {TEMPLATE_ID} from "@/config/setting";
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 获取字典数据
// const userTypeData = getDictionaryOptions('userType');
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: User | null;
// 全部机构
organizationList: Organization[];
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<User>({
type: undefined,
userId: undefined,
username: '',
nickname: '',
realName: '',
companyName: '',
sex: undefined,
sexName: undefined,
roles: [],
email: '',
phone: '',
mobile: '',
password: '',
introduction: '',
organizationId: undefined,
birthday: '',
idCard: '',
comments: '',
gradeName: '',
isAdmin: true,
gradeId: undefined,
templateId: TEMPLATE_ID
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
username: [
{
required: true,
type: 'string',
validator: (_rule: Rule, value: string) => {
return new Promise<void>((resolve, reject) => {
if (!value) {
return reject('请输入管理员账号');
}
checkExistence('username', value, props.data?.userId)
.then(() => {
reject('账号已经存在');
})
.catch(() => {
resolve();
});
});
},
trigger: 'blur'
}
],
nickname: [
{
required: true,
message: '请输入昵称',
type: 'string',
trigger: 'blur'
}
],
realName: [
{
required: true,
message: '请输入真实姓名',
type: 'string',
trigger: 'blur'
}
],
// sex: [
// {
// required: true,
// message: '请选择性别',
// type: 'string',
// trigger: 'blur'
// }
// ],
roles: [
{
required: true,
message: '请选择角色',
type: 'array',
trigger: 'blur'
}
],
email: [
{
pattern: emailReg,
message: '邮箱格式不正确',
type: 'string',
trigger: 'blur'
}
],
password: [
{
required: true,
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (isUpdate.value || /^[\S]{5,18}$/.test(value)) {
return Promise.resolve();
}
return Promise.reject('密码必须为5-18位非空白字符');
},
trigger: 'blur'
}
],
phone: [
{
required: true,
pattern: phoneReg,
message: '手机号格式不正确',
type: 'string',
trigger: 'blur'
}
]
});
const chooseGradeId = (data: Grade) => {
form.gradeName = data.name;
form.gradeId = data.gradeId;
};
const chooseSex = (data: any) => {
form.sex = data.key;
form.sexName = data.label;
};
const updateIsAdmin = (value: boolean) => {
form.isAdmin = value;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateUser : addUser;
form.username = form.phone;
form.nickname = form.realName;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields({
...props.data,
password: ''
});
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>

View File

@@ -3,7 +3,7 @@
<ele-modal
:width="520"
:footer="null"
title="导入用户"
title="用户批量导入"
:visible="visible"
@update:visible="updateVisible"
>
@@ -23,10 +23,10 @@
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a
href="https://oss.wsdns.cn/20200610/用户导入模板.xlsx"
href="https://server.websoft.top/api/system/user/import/template"
download="用户导入模板.xlsx"
>
下载模板
下载导入模板
</a>
</div>
</ele-modal>

View File

@@ -0,0 +1,603 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="userId"
:columns="columns"
:datasource="datasource"
class="sys-org-table"
:scroll="{ x: 1300 }"
:where="defaultWhere"
:customRow="customRow"
cache-key="proSystemUserTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined/>
</template>
<span>添加</span>
</a-button>
<a-button class="ele-btn-icon" @click="openImport()">
<template #icon>
<cloud-upload-outlined/>
</template>
<span>导入</span>
</a-button>
<a-button class="ele-btn-icon" @click="exportData()" :loading="exportLoading">
<template #icon>
<download-outlined/>
</template>
<span>导出</span>
</a-button>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'avatar'">
<a-avatar
:size="30"
:src="`${record.avatar}`"
style="margin-right: 4px"
>
<template #icon>
<UserOutlined/>
</template>
</a-avatar>
</template>
<template v-if="column.key === 'nickname'">
<div>{{ record.nickname }}</div>
<div class="text-gray-400">{{ record.realName }}</div>
</template>
<template v-if="column.key === 'phone'">
<span v-if="hasRole('superAdmin')">{{ record.phone }}</span>
<span v-else>{{ record.phone }}</span>
</template>
<template v-if="column.key === 'roles'">
<a-tag v-for="item in record.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</template>
<template v-if="column.key === 'platform'">
<WechatOutlined v-if="record.platform === 'MP-WEIXIN'"/>
<Html5Outlined v-if="record.platform === 'H5'"/>
<ChromeOutlined v-if="record.platform === 'WEB'"/>
</template>
<template v-if="column.key === 'balance'">
<span class="ele-text-success">
{{ formatNumber(record.balance) }}
</span>
</template>
<template v-if="column.key === 'expendMoney'">
<span class="ele-text-warning">
{{ formatNumber(record.expendMoney) }}
</span>
</template>
<template v-if="column.key === 'isAdmin'">
<a-switch
:checked="record.isAdmin == 1"
@change="updateIsAdmin(record)"
/>
</template>
<template v-if="column.key === 'action'">
<div>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
placement="topRight"
title="确定要删除此用户吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</div>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<user-edit
v-model:visible="showEdit"
:data="current"
:organization-list="data"
@done="reload"
/>
<!-- 导入弹窗 -->
<user-import v-model:visible="showImport" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, reactive, watch} from 'vue';
import {message, Modal} from 'ant-design-vue/es';
import {
PlusOutlined,
UserOutlined,
Html5Outlined,
ChromeOutlined,
WechatOutlined,
CloudUploadOutlined,
DownloadOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import {messageLoading, formatNumber} from 'ele-admin-pro/es';
import UserEdit from './components/user-edit.vue';
import UserImport from './components/user-import.vue';
import {toDateString} from 'ele-admin-pro';
import { utils, writeFile } from 'xlsx';
import dayjs from 'dayjs';
import {
pageShopUser,
removeShopUser,
removeBatchShopUser,
updateShopUser,
listShopUser
} from '@/api/shop/shopUser';
import type {ShopUser, ShopUserParam} from '@/api/shop/shopUser/model';
import {toTreeData, uuid} from 'ele-admin-pro';
import {listRoles} from '@/api/system/role';
import {listOrganizations} from '@/api/system/organization';
import {Organization} from '@/api/system/organization/model';
import {hasRole} from '@/utils/permission';
import {getPageTitle} from "@/utils/common";
import router from "@/router";
import {getTenantId} from "@/utils/domain";
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<Organization[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格选中数据
const selection = ref<ShopUser[]>([]);
// 当前编辑数据
const current = ref<ShopUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示用户详情
const showInfo = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
// 导出加载状态
const exportLoading = ref(false);
const userType = ref<number>();
const searchText = ref('');
// 加载角色
const roles = ref<any[]>([]);
// 加载机构
listOrganizations()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d) => {
d.key = d.organizationId;
d.value = d.organizationId;
d.title = d.organizationName;
if (typeof d.key === 'number') {
eks.push(d.key);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list,
idField: 'organizationId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].key === 'number') {
selectedRowKeys.value = [list[0].key];
}
// current.value = list[0];
} else {
selectedRowKeys.value = [];
// current.value = null;
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'userId',
width: 90,
showSorterTooltip: false
},
{
title: '昵称/姓名',
dataIndex: 'nickname',
key: 'nickname',
align: 'center',
showSorterTooltip: false
},
{
title: '手机号码',
dataIndex: 'phone',
align: 'center',
showSorterTooltip: false
},
{
title: '积分',
dataIndex: 'points',
align: 'center',
width: 100
},
{
title: '余额',
dataIndex: 'balance',
align: 'center',
width: 100
},
// {
// title: '角色',
// dataIndex: 'roles',
// key: 'roles',
// align: 'center'
// },
{
title: '备注',
dataIndex: 'comments',
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
align: 'center',
sorter: true,
customRender: ({text}) => {
return text === 1
? createVNode(
'span',
{
class: 'text-red-400'
},
'封号'
)
: createVNode(
'span',
{
class: 'text-gray-400'
},
'正常'
);
}
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
align: 'center',
showSorterTooltip: false,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center'
}
]);
// 默认搜索条件
const defaultWhere = reactive({
username: '',
nickname: ''
});
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
where = {};
where.roleId = filters.roles;
where.keywords = searchText.value;
return pageShopUser({page, limit, ...where, ...orders});
};
/* 搜索 */
const reload = (where?: ShopUserParam) => {
selection.value = [];
tableRef?.value?.reload({where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开用户详情弹窗 */
const openInfo = (row?: ShopUser) => {
current.value = row ?? null;
showInfo.value = true;
};
/* 打开编辑弹窗 */
const openImport = () => {
showImport.value = true;
};
/* 导出数据 */
const exportData = async () => {
exportLoading.value = true;
try {
// 定义表头
const array: (string | number)[][] = [
[
'用户ID',
'账号',
'昵称',
'真实姓名',
'手机号',
'邮箱',
'性别',
'状态',
'注册时间'
]
];
// 构建查询参数,使用当前搜索条件
const params = {
keywords: searchText.value,
isAdmin: 0
};
// 获取用户列表数据
const list = await listShopUser(params);
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
exportLoading.value = false;
return;
}
// 将数据转换为Excel行
list.forEach((user: ShopUser) => {
array.push([
`${user.userId || ''}`,
`${user.username || ''}`,
`${user.nickname || ''}`,
`${user.realName || ''}`,
`${user.phone || ''}`,
`${user.email || ''}`,
`${user.sex == 1 ? '男' : '女'}`,
`${user.status === 0 ? '正常' : '冻结'}`,
`${user.createTime || ''}`
]);
});
// 生成Excel文件
const sheetName = `shop_user_${getTenantId()}_${dayjs(new Date()).format('YYYYMMDD')}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 }, // 用户ID
{ wch: 15 }, // 账号
{ wch: 12 }, // 昵称
{ wch: 12 }, // 真实姓名
{ wch: 15 }, // 手机号
{ wch: 20 }, // 邮箱
{ wch: 8 }, // 性别
{ wch: 15 }, // 所属部门
{ wch: 20 }, // 角色
{ wch: 8 }, // 状态
{ wch: 20 } // 注册时间
];
message.loading('正在生成Excel文件...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
exportLoading.value = false;
message.destroy();
message.success(`成功导出 ${list.length} 条记录`);
}, 1000);
} catch (error: any) {
exportLoading.value = false;
message.error(error.message || '导出失败');
}
};
const handleTabs = (e) => {
userType.value = Number(e.target.value);
reload();
};
/* 删除单个 */
const remove = (row: ShopUser) => {
const hide = messageLoading('请求中..', 0);
removeShopUser(row.userId)
.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 = messageLoading('请求中..', 0);
removeShopUser(selection.value.map((d) => d.userId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 重置用户密码 */
const resetPsw = (row: ShopUser) => {
Modal.confirm({
title: '提示',
content: '确定要重置此用户的密码吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
const password = uuid(8);
updateShopUser({
...row,
password: password
})
.then((msg) => {
hide();
message.success(msg + ',新密码:' + password);
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 修改用户状态 */
const updateIsAdmin = (row: ShopUser) => {
updateShopUser(row)
.then((msg) => {
message.success(msg);
})
.catch((e) => {
message.error(e.message);
});
};
/* 自定义行属性 */
const customRow = (record: ShopUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
const query = async () => {
const info = await listRoles({})
if (info) {
roles.value = info
}
}
watch(
() => router.currentRoute.value.query,
() => {
query();
},
{immediate: true}
);
</script>
<script lang="ts">
export default {
name: 'ShopUser'
};
</script>
<style lang="less" scoped>
.sys-org-table {
:deep(.ant-table) {
.ant-table-thead > tr > th {
background: #fafafa;
font-weight: 600;
color: #262626;
border-bottom: 2px solid #f0f0f0;
}
.ant-table-tbody > tr > td {
padding: 12px 8px;
border-bottom: 1px solid #f5f5f5;
}
.ant-table-tbody > tr:hover > td {
background: #f8f9ff;
}
.ant-tag {
margin: 0;
border-radius: 4px;
font-size: 12px;
padding: 2px 8px;
}
}
}
.ele-text-primary {
color: #1890ff;
&:hover {
color: #40a9ff;
}
}
.ele-text-danger {
color: #ff4d4f;
&:hover {
color: #ff7875;
}
}
</style>

View File

@@ -225,12 +225,7 @@
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopUserCouponId: undefined,
shopUserCouponName: '',
status: 0,
comments: '',
sortNumber: 100
updateTime: undefined
});
/* 更新visible */

View File

@@ -115,12 +115,7 @@
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopUserRefereeId: undefined,
shopUserRefereeName: '',
status: 0,
comments: '',
sortNumber: 100
updateTime: undefined
});
/* 更新visible */