chore(config): 添加项目配置文件和隐私协议

- 添加 .editorconfig 文件统一代码风格
- 添加 .env.development 和 .env.example 环境配置文件
- 添加 .eslintignore 和 .eslintrc.js 代码检查配置
- 添加 .gitignore 版本控制忽略文件配置
- 添加 .prettierignore 格式化忽略配置
- 添加隐私协议HTML文件
- 添加API密钥管理组件基础结构
This commit is contained in:
2026-01-26 14:05:01 +08:00
commit 482e2a2718
1192 changed files with 238401 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
<!-- 搜索表单 -->
<template>
<a-space style="flex-wrap: wrap">
<a-button type="text" @click="openUrl(`/website/field`)"
>字段扩展
</a-button>
<a-button type="text" @click="openUrl('/website/dict')">字典管理 </a-button>
<a-button type="text" @click="openUrl('/website/domain')"
>域名管理
</a-button>
<a-button type="text" @click="openUrl('/website/model')"
>模型管理
</a-button>
<a-button type="text" @click="openUrl('/website/form')">表单管理 </a-button>
<a-button type="text" @click="openUrl('/website/lang')">国际化 </a-button>
<a-button type="text" @click="openUrl('/website/setting')"
>网站设置
</a-button>
<a-button type="text" class="ele-btn-icon" @click="clearSiteInfoCache">
清除缓存
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { watch, nextTick } from 'vue';
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
import { openUrl } from '@/utils/common';
import { message } from 'ant-design-vue';
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
const emit = defineEmits<{
(e: 'add'): void;
}>();
const add = () => {
emit('add');
};
// 清除缓存
const clearSiteInfoCache = () => {
removeSiteInfoCache(
'SiteInfo:' + localStorage.getItem('TenantId') + '*'
).then((msg) => {
if (msg) {
message.success(msg);
}
});
};
nextTick(() => {
if (localStorage.getItem('NotActive')) {
// IsActive.value = false
}
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,408 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑小程序' : '创建小程序'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:confirm-loading="loading"
@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="Logo" name="avatar">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@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
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="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-divider style="margin-bottom: 24px" />-->
</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 { 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, 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';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: CmsWebsite | 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 websiteQrcode = ref<ItemType[]>([]);
const oldDomain = ref();
const files = ref<ItemType[]>([]);
// 用户信息
const form = reactive<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'
}
],
// 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,
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 onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.websiteLogo = '';
};
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);
/* 保存编辑 */
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(() => {});
};
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.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>

View File

@@ -0,0 +1,316 @@
<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

@@ -0,0 +1,345 @@
<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'));
});
// 邀请链接需要带上 tenantId避免未登录用户打开链接时后端无法识别租户导致角色/权限初始化失败
const tenantId = computed(() => {
const tid = localStorage.getItem('TenantId');
return tid ? Number(tid) : undefined;
});
// 生成邀请链接
const invitationLink = computed(() => {
const baseUrl = window.location.origin;
const params = new URLSearchParams();
params.set('inviter', String(inviterId.value));
if (tenantId.value) {
params.set('tenantId', String(tenantId.value));
}
return `${baseUrl}/dealer/register?${params.toString()}`;
});
// 复制链接
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

@@ -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,42 @@
<template>
<a-card title="管理员" style="margin-bottom: 20px">
<div class="title flex flex-col">
<div class="text-gray-400 pb-2">系统所有者拥有全部权限</div>
</div>
<div
v-if="item"
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>
<a>更换</a>
</div>
</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 item = ref<User>();
const reload = async () => {
const list = await listUsers({
isSuperAdmin: true
});
console.log(list);
if (list.length > 0) {
item.value = list[0];
}
};
onMounted(() => {
reload();
});
</script>

View File

@@ -0,0 +1,275 @@
<!-- 管理员编辑弹窗 -->
<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 v-if="!isUpdate" label="登录密码" name="password">
<a-input-password
:maxlength="20"
v-model:value="form.password"
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="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 { 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'
}
],
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 chooseSex = (data: any) => {
form.sex = data.key;
form.sexName = data.label;
};
/* 保存编辑 */
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

@@ -0,0 +1,439 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<SuperAdmin />
<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="openInvitation"
>
<template #icon>
<plus-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 === '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">
{{ 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 @click="resetPsw(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" />
<!-- 用户详情 -->
<user-info v-model:visible="showInfo" :data="current" @done="reload" />
<!-- 邀请注册弹窗 -->
<invitation-modal
v-model:visible="showInvitation"
:inviter-id="currentUserId"
/>
</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,
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 InvitationModal from './components/invitation-modal.vue';
import { toDateString } from 'ele-admin-pro';
import {
pageUsers,
removeUser,
updateUserPassword,
updateUser
} from '@/api/system/user';
import type { User, UserParam } from '@/api/system/user/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 SuperAdmin from './components/super-admin.vue';
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<Organization[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格选中数据
const selection = ref<User[]>([]);
// 当前编辑数据
const current = ref<User | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示用户详情
const showInfo = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
// 是否显示邀请注册弹窗
const showInvitation = ref(false);
const searchText = ref('');
// 当前用户ID
const currentUserId = ref<number>();
// 加载角色
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: 'realName',
key: 'realName',
align: 'center',
showSorterTooltip: false
},
{
title: '所属部门',
dataIndex: 'organizationName',
key: 'organizationName',
align: 'center'
},
{
title: '角色',
dataIndex: 'roles',
key: 'roles',
align: 'center'
},
{
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;
where.isAdmin = 1;
return pageUsers({ page, limit, ...where, ...orders });
};
/* 搜索 */
const reload = (where?: UserParam) => {
selection.value = [];
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: User) => {
const hide = messageLoading('请求中..', 0);
removeUser(row.userId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 重置用户密码 */
const resetPsw = (row: User) => {
Modal.confirm({
title: '提示',
content: '确定要重置此用户的密码吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
const password = uuid(8);
updateUserPassword(row.userId, password)
.then((msg) => {
hide();
message.success(msg + ',新密码:' + password);
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 修改用户状态 */
const updateIsAdmin = (row: User) => {
row.isAdmin = !row.isAdmin;
updateUser(row)
.then((msg) => {
message.success(msg);
})
.catch((e) => {
message.error(e.message);
});
};
/* 自定义行属性 */
const customRow = (record: User) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
const query = async () => {
const info = await listRoles({});
if (info) {
roles.value = info;
}
};
/* 打开邀请注册弹窗 */
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,
() => {
query();
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'SystemAdmin'
};
</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

@@ -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,958 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1200"
: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="优惠券名称" name="name">
<a-input placeholder="请输入优惠券名称" v-model:value="form.name" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="优惠券类型" name="type">
<a-select
v-model:value="form.type"
placeholder="请选择优惠券类型"
@change="onTypeChange"
>
<a-select-option :value="10">
<div class="coupon-type-option">
<a-tag color="red">满减券</a-tag>
<span>满足条件减免金额</span>
</div>
</a-select-option>
<a-select-option :value="20">
<div class="coupon-type-option">
<a-tag color="orange">折扣券</a-tag>
<span>按比例折扣</span>
</div>
</a-select-option>
<a-select-option :value="30">
<div class="coupon-type-option">
<a-tag color="green">免费券</a-tag>
<span>免费使用</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="优惠券描述" name="description">
<a-textarea
placeholder="请输入优惠券描述"
v-model:value="form.description"
:rows="3"
:maxlength="200"
show-count
/>
</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="minPrice">
<a-input-number
:min="0"
:precision="2"
placeholder="请输入最低消费金额"
v-model:value="form.minPrice"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<!-- 满减券设置 -->
<a-form-item
v-if="form.type === 10"
label="减免金额"
name="reducePrice"
>
<a-input-number
:min="0"
:precision="2"
placeholder="请输入减免金额"
v-model:value="form.reducePrice"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
<!-- 折扣券设置 -->
<a-form-item v-if="form.type === 20" label="折扣率" name="discount">
<a-input-number
:min="0.1"
:max="99.9"
:precision="1"
placeholder="请输入折扣率"
v-model:value="form.discount"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</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="expireType">
<a-radio-group
v-model:value="form.expireType"
@change="onExpireTypeChange"
>
<a-radio :value="10">
<a-tag color="blue">领取后生效</a-tag>
<span style="margin-left: 8px">用户领取后开始计时</span>
</a-radio>
<a-radio :value="20">
<a-tag color="purple">固定时间</a-tag>
<span style="margin-left: 8px">指定有效期时间段</span>
</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
v-if="form.expireType === 10"
label="有效天数"
name="expireDay"
>
<a-input-number
:min="1"
placeholder="请输入有效天数"
v-model:value="form.expireDay"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<div v-if="form.expireType === 20" class="fixed-time-config">
<a-alert
message="固定时间配置"
description="设置优惠券的固定有效期时间段"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="有效期开始时间" name="startTime">
<a-date-picker
placeholder="请选择开始时间"
v-model:value="form.startTime"
style="width: 100%"
show-time
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="有效期结束时间" name="endTime">
<a-date-picker
placeholder="请选择结束时间"
v-model:value="form.endTime"
style="width: 100%"
show-time
/>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 适用范围 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">适用范围</span>
</a-divider>
<a-form-item label="适用范围" name="applyRange">
<a-radio-group
v-model:value="form.applyRange"
@change="onApplyRangeChange"
>
<a-radio :value="10">
<a-tag color="cyan">全部商品</a-tag>
<span style="margin-left: 8px">适用于所有商品</span>
</a-radio>
<a-radio :value="20">
<a-tag color="geekblue">指定商品</a-tag>
<span style="margin-left: 8px">仅适用于指定商品</span>
</a-radio>
<a-radio :value="30">
<a-tag color="purple">指定分类</a-tag>
<span style="margin-left: 8px">适用于指定商品分类</span>
</a-radio>
</a-radio-group>
</a-form-item>
<div v-if="form.applyRange === 30" class="apply-range-config">
<a-alert
message="分类限制配置"
description="选择优惠券适用的商品分类"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-form-item label="限制分类">
<a-cascader
v-model:value="applyCateListValue"
:options="goodsCateList"
multiple
placeholder="请选择商品分类"
:fieldNames="{ label: 'title', value: 'categoryId' }"
style="width: 100%"
/>
</a-form-item>
</div>
<div v-if="form.applyRange === 20" class="apply-range-config">
<a-alert
message="商品限制配置"
description="选择优惠券适用的具体商品"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-form-item label="限制商品">
<a-select
mode="multiple"
v-model:value="applyItemListValue"
placeholder="请选择商品"
style="width: 100%"
:filter-option="false"
:show-search="true"
@search="searchGoods"
>
<a-select-option
v-for="(item, index) in goodsList"
:key="index"
:value="item.goodsId"
>
{{ item.name }}
</a-select-option>
</a-select>
</a-form-item>
</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="totalCount">
<a-input-number
:min="-1"
placeholder="请输入发放总数量"
v-model:value="form.totalCount"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
<div style="color: #999; font-size: 12px; margin-top: 4px">
-1 表示无限制
</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="每人限领数量" name="limitPerUser">
<a-input-number
:min="-1"
placeholder="请输入每人限领数量"
v-model:value="form.limitPerUser"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
<div style="color: #999; font-size: 12px; margin-top: 4px">
-1 表示无限制
</div>
</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="enabled">
<a-switch
v-model:checked="form.enabled"
:checked-value="1"
:un-checked-value="0"
checked-children="启用"
un-checked-children="禁用"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="显示状态" name="status">
<a-switch
v-model:checked="form.status"
:checked-value="0"
:un-checked-value="1"
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>
<!-- 优惠券预览 -->
<div class="coupon-preview" v-if="form.name && form.type">
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">优惠券预览</span>
</a-divider>
<div class="coupon-card">
<div class="coupon-header">
<div class="coupon-type">
<a-tag :color="getCouponTypeColor()">{{
getCouponTypeName()
}}</a-tag>
</div>
<div class="coupon-value">
{{ getCouponValueText() }}
</div>
</div>
<div class="coupon-body">
<div class="coupon-name">{{ form.name }}</div>
<div class="coupon-desc">{{ form.description || '暂无描述' }}</div>
<div class="coupon-condition">
{{ form.minPrice || 0 }}元可用
</div>
</div>
<div class="coupon-footer">
<div class="coupon-expire">
{{ getExpireText() }}
</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 } from 'ele-admin-pro';
import { addShopCoupon, updateShopCoupon } from '@/api/shop/shopCoupon';
import { ShopCoupon } from '@/api/shop/shopCoupon/model';
import { FormInstance } from 'ant-design-vue/es/form';
import { ShopGoods } from '@/api/shop/shopGoods/model';
import { listShopGoods } from '@/api/shop/shopGoods';
import { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model';
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory';
import dayjs from 'dayjs';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopCoupon | 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 form = reactive<ShopCoupon>({
id: undefined,
name: '',
description: '',
type: 10,
reducePrice: undefined,
discount: undefined,
minPrice: 0,
expireType: 10,
expireDay: 30,
startTime: undefined,
endTime: undefined,
applyRange: 10,
applyRangeConfig: '',
isExpire: 0,
sortNumber: 100,
status: 0,
deleted: 0,
userId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
totalCount: -1,
issuedCount: 0,
limitPerUser: 1,
enabled: 1
});
// 商品分类列表
const goodsCateList = ref<ShopGoodsCategory[]>([]);
// 商品列表
const goodsList = ref<ShopGoods[]>([]);
// 适用分类值
const applyCateListValue = ref<number[]>([]);
// 适用商品值
const applyItemListValue = ref<number[]>([]);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
message: '请输入优惠券名称',
trigger: 'blur'
},
{
min: 2,
max: 50,
message: '优惠券名称长度应在2-50个字符之间',
trigger: 'blur'
}
],
type: [
{
required: true,
message: '请选择优惠券类型',
trigger: 'change'
}
],
reducePrice: [
{
validator: (rule: any, value: any) => {
if (form.type === 10 && (!value || value <= 0)) {
return Promise.reject('满减券减免金额必须大于0');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
discount: [
{
validator: (rule: any, value: any) => {
if (form.type === 20 && (!value || value <= 0 || value >= 100)) {
return Promise.reject('折扣率必须在0-100之间');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
minPrice: [
{
required: true,
message: '请输入最低消费金额',
trigger: 'blur'
}
],
expireType: [
{
required: true,
message: '请选择到期类型',
trigger: 'change'
}
],
expireDay: [
{
validator: (rule: any, value: any) => {
if (form.expireType === 10 && (!value || value <= 0)) {
return Promise.reject('有效天数必须大于0');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
startTime: [
{
validator: (rule: any, value: any) => {
if (form.expireType === 20 && !value) {
return Promise.reject('请选择有效期开始时间');
}
return Promise.resolve();
},
trigger: 'change'
}
],
endTime: [
{
validator: (rule: any, value: any) => {
if (form.expireType === 20 && !value) {
return Promise.reject('请选择有效期结束时间');
}
if (
form.expireType === 20 &&
value &&
form.startTime &&
dayjs(value).isBefore(dayjs(form.startTime))
) {
return Promise.reject('结束时间不能早于开始时间');
}
return Promise.resolve();
},
trigger: 'change'
}
],
totalCount: [
{
required: true,
message: '请输入发放总数量',
trigger: 'blur'
}
],
limitPerUser: [
{
required: true,
message: '请输入每人限领数量',
trigger: 'blur'
}
]
});
/* 优惠券类型改变 */
const onTypeChange = (value: number) => {
// 清空相关字段
if (value !== 10) {
form.reducePrice = undefined;
}
if (value !== 20) {
form.discount = undefined;
}
};
/* 到期类型改变 */
const onExpireTypeChange = (e: any) => {
const value = e.target.value;
if (value === 10) {
form.startTime = undefined;
form.endTime = undefined;
form.expireDay = 30;
} else {
form.expireDay = undefined;
}
};
/* 适用范围改变 */
const onApplyRangeChange = (e: any) => {
const value = e.target.value;
applyCateListValue.value = [];
applyItemListValue.value = [];
form.applyRangeConfig = '';
};
/* 搜索商品 */
const searchGoods = async (value: string) => {
if (value && value.trim()) {
try {
const res = await listShopGoods({ keywords: value.trim() });
goodsList.value = res || [];
console.log('搜索到的商品:', goodsList.value);
} catch (e) {
console.error('搜索商品失败:', e);
goodsList.value = [];
}
}
};
/* 获取优惠券类型颜色 */
const getCouponTypeColor = () => {
const colorMap = {
10: 'red',
20: 'orange',
30: 'green'
};
return colorMap[form.type] || 'blue';
};
/* 获取优惠券类型名称 */
const getCouponTypeName = () => {
const nameMap = {
10: '满减券',
20: '折扣券',
30: '免费券'
};
return nameMap[form.type] || '优惠券';
};
/* 获取优惠券价值文本 */
const getCouponValueText = () => {
switch (form.type) {
case 10:
return `${form.reducePrice || 0}`;
case 20:
return `${form.discount || 0}`;
case 30:
return '免费';
default:
return '优惠';
}
};
/* 获取有效期文本 */
const getExpireText = () => {
if (form.expireType === 10) {
return `领取后${form.expireDay || 0}天内有效`;
} else {
const start = form.startTime
? dayjs(form.startTime).format('YYYY.MM.DD')
: '';
const end = form.endTime ? dayjs(form.endTime).format('YYYY.MM.DD') : '';
return start && end ? `${start} - ${end}` : '请设置有效期';
}
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
// 处理时间字段转换
if (formData.startTime && dayjs.isDayjs(formData.startTime)) {
formData.startTime = formData.startTime.format('YYYY-MM-DD HH:mm:ss');
}
if (formData.endTime && dayjs.isDayjs(formData.endTime)) {
formData.endTime = formData.endTime.format('YYYY-MM-DD HH:mm:ss');
}
// 处理适用范围配置
if (form.applyRange === 20 && applyItemListValue.value.length) {
formData.couponApplyItemList = applyItemListValue.value.map((pk) => ({
pk,
type: 0
}));
} else {
formData.couponApplyItemList = [];
}
if (form.applyRange === 30 && applyCateListValue.value.length) {
formData.couponApplyCateList = applyCateListValue.value.map(
(cateId) => ({
cateId,
cateLevel: 0
})
);
} else {
formData.couponApplyCateList = [];
}
const saveOrUpdate = isUpdate.value ? updateShopCoupon : addShopCoupon;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 获取商品列表 */
const getGoodsList = async () => {
try {
const res = await listShopGoods({ pageSize: 50 });
goodsList.value = res || [];
console.log('获取到的商品列表:', goodsList.value);
} catch (e) {
console.error('获取商品列表失败:', e);
goodsList.value = [];
}
};
/* 获取商品分类列表 */
const getGoodsCateList = async () => {
try {
const res = await listShopGoodsCategory();
goodsCateList.value = res || [];
console.log('获取到的商品分类列表:', goodsCateList.value);
} catch (e) {
console.error('获取商品分类列表失败:', e);
goodsCateList.value = [];
}
};
watch(
() => props.visible,
async (visible) => {
if (visible) {
await getGoodsList();
await getGoodsCateList();
if (props.data) {
assignObject(form, props.data);
// 处理时间字段转换
if (props.data.startTime) {
form.startTime = dayjs(props.data.startTime);
}
if (props.data.endTime) {
form.endTime = dayjs(props.data.endTime);
}
// 处理适用范围数据
if (
props.data.couponApplyCateList &&
props.data.couponApplyCateList.length > 0
) {
applyCateListValue.value = props.data.couponApplyCateList.map(
(item) => item.cateId
);
}
if (
props.data.couponApplyItemList &&
props.data.couponApplyItemList.length > 0
) {
applyItemListValue.value = props.data.couponApplyItemList.map(
(item) => item.pk
);
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
id: undefined,
name: '',
description: '',
type: 10,
reducePrice: undefined,
discount: undefined,
minPrice: 0,
expireType: 10,
expireDay: 30,
startTime: undefined,
endTime: undefined,
applyRange: 10,
applyRangeConfig: '',
isExpire: 0,
sortNumber: 100,
status: 0,
deleted: 0,
userId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
totalCount: -1,
issuedCount: 0,
limitPerUser: 1,
enabled: 1
});
isUpdate.value = false;
}
} else {
applyCateListValue.value = [];
applyItemListValue.value = [];
resetFields();
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
.coupon-type-option {
display: flex;
align-items: center;
.ant-tag {
margin-right: 8px;
}
span {
color: #666;
font-size: 12px;
}
}
.fixed-time-config,
.apply-range-config {
background: #fafafa;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
}
.coupon-preview {
margin-top: 24px;
.coupon-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 20px;
color: white;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(30px, -30px);
}
.coupon-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.coupon-value {
font-size: 24px;
font-weight: bold;
}
}
.coupon-body {
.coupon-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.coupon-desc {
font-size: 14px;
opacity: 0.9;
margin-bottom: 12px;
}
.coupon-condition {
font-size: 12px;
opacity: 0.8;
}
}
.coupon-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
.coupon-expire {
font-size: 12px;
opacity: 0.8;
}
}
}
}
: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

@@ -0,0 +1,744 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<a-space>
<a-button type="primary" @click="openEdit()">
<template #icon>
<PlusOutlined />
</template>
新增优惠券
</a-button>
<a-button @click="reload()">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
</a-space>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="优惠券名称">
<a-input
v-model:value="searchForm.name"
placeholder="请输入优惠券名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="优惠券类型">
<a-select
v-model:value="searchForm.type"
placeholder="请选择类型"
allow-clear
style="width: 150px"
>
<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-select
v-model:value="searchForm.expireType"
placeholder="请选择到期类型"
allow-clear
style="width: 150px"
>
<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.isExpire"
placeholder="请选择状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="0">未过期</a-select-option>
<a-select-option :value="1">已过期</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<ClearOutlined />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<!-- 批量操作区域 -->
<div v-if="selection.length > 0" class="batch-actions">
<a-alert
:message="`已选择 ${selection.length} 项`"
type="info"
show-icon
style="margin-bottom: 16px"
>
<template #action>
<a-space>
<a-button size="small" @click="clearSelection">取消选择</a-button>
<a-popconfirm
title="确定要删除选中的优惠券吗?"
@confirm="removeBatch"
>
<a-button size="small" danger>批量删除</a-button>
</a-popconfirm>
</a-space>
</template>
</a-alert>
</div>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:row-selection="rowSelection"
:scroll="{ x: 1800 }"
tool-class="ele-toolbar-form"
class="coupon-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="coupon-name">
<a-typography-text strong>{{ record.name }}</a-typography-text>
<div class="coupon-description">{{
record.description || '暂无描述'
}}</div>
</div>
</template>
<template v-if="column.key === 'type'">
<a-tag :color="getCouponTypeColor(record.type)">
{{ getCouponTypeText(record.type) }}
</a-tag>
</template>
<template v-if="column.key === 'value'">
<div class="coupon-value">
<template v-if="record.type === 10">
<span class="value-amount"
>¥{{ Number(record.reducePrice).toFixed(2) || '0.00' }}</span
>
<div class="value-condition"
>¥{{
Number(record.minPrice).toFixed(2) || '0.00'
}}可用</div
>
</template>
<template v-else-if="record.type === 20">
<span class="value-discount">{{ record.discount }}</span>
<div class="value-condition"
>¥{{
Number(record.minPrice)?.toFixed(2) || '0.00'
}}可用</div
>
</template>
<template v-else-if="record.type === 30">
<span class="value-free">免费券</span>
<div class="value-condition"
>¥{{
Number(record.minPrice)?.toFixed(2) || '0.00'
}}可用</div
>
</template>
</div>
</template>
<template v-if="column.key === 'expireInfo'">
<div class="expire-info">
<a-tag :color="record.expireType === 10 ? 'blue' : 'green'">
{{ record.expireType === 10 ? '领取后生效' : '固定时间' }}
</a-tag>
<div class="expire-detail">
<template v-if="record.expireType === 10">
{{ record.expireDay }}天有效
</template>
<template v-else>
{{ formatDate(record.startTime) }}
{{ formatDate(record.endTime) }}
</template>
</div>
</div>
</template>
<template v-if="column.key === 'applyRange'">
<a-tag :color="getApplyRangeColor(record.applyRange)">
{{ getApplyRangeText(record.applyRange) }}
</a-tag>
</template>
<template v-if="column.key === 'usage'">
<div class="usage-info">
<a-progress
:percent="getUsagePercent(record)"
:stroke-color="getUsageColor(record)"
size="small"
/>
<div class="usage-text">
已发放: {{ record.issuedCount || 0 }}
<template v-if="record.totalCount !== -1">
/ {{ record.totalCount }}
</template>
<template v-else> (无限制) </template>
</div>
</div>
</template>
<template v-if="column.key === 'isExpire'">
<a-tag :color="record.isExpire === 0 ? 'success' : 'error'">
{{ record.isExpire === 0 ? '未过期' : '已过期' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-tooltip title="编辑">
<a-button type="link" size="small" @click="openEdit(record)">
<template #icon>
<EditOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip title="复制">
<a-button type="link" size="small" @click="copyRecord(record)">
<template #icon>
<CopyOutlined />
</template>
</a-button>
</a-tooltip>
<a-popconfirm
title="确定要删除此优惠券吗?"
@confirm="remove(record)"
>
<a-tooltip title="删除">
<a-button type="link" size="small" danger>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopCouponEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, reactive, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
ExclamationCircleOutlined,
PlusOutlined,
ReloadOutlined,
SearchOutlined,
ClearOutlined,
EditOutlined,
DeleteOutlined,
CopyOutlined
} 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 { getPageTitle } from '@/utils/common';
import ShopCouponEdit from './components/shopCouponEdit.vue';
import {
pageShopCoupon,
removeShopCoupon,
removeBatchShopCoupon
} from '@/api/shop/shopCoupon';
import type {
ShopCoupon,
ShopCouponParam
} from '@/api/shop/shopCoupon/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopCoupon[]>([]);
// 当前编辑数据
const current = ref<ShopCoupon | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 加载状态
const loading = ref(false);
// 搜索表单
const searchForm = reactive<ShopCouponParam>({
keywords: '',
name: undefined,
type: undefined
});
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
const params = {
...where,
...searchForm,
...orders,
page,
limit
};
if (filters) {
Object.assign(params, filters);
}
return pageShopCoupon(params);
};
// 行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selection.value.map((item) => item.id),
onChange: (
selectedRowKeys: (string | number)[],
selectedRows: ShopCoupon[]
) => {
selection.value = selectedRows;
},
onSelect: (record: ShopCoupon, selected: boolean) => {
if (selected) {
selection.value.push(record);
} else {
const index = selection.value.findIndex(
(item) => item.id === record.id
);
if (index > -1) {
selection.value.splice(index, 1);
}
}
},
onSelectAll: (
selected: boolean,
selectedRows: ShopCoupon[],
changeRows: ShopCoupon[]
) => {
if (selected) {
changeRows.forEach((row) => {
if (!selection.value.find((item) => item.id === row.id)) {
selection.value.push(row);
}
});
} else {
changeRows.forEach((row) => {
const index = selection.value.findIndex((item) => item.id === row.id);
if (index > -1) {
selection.value.splice(index, 1);
}
});
}
}
}));
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 80,
fixed: 'left'
},
{
title: '优惠券信息',
dataIndex: 'name',
key: 'name',
align: 'left',
width: 250,
fixed: 'left',
ellipsis: true
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 100
},
{
title: '优惠价值',
dataIndex: 'value',
key: 'value',
align: 'center',
width: 150
},
{
title: '有效期信息',
dataIndex: 'expireInfo',
key: 'expireInfo',
align: 'center',
width: 180
},
{
title: '适用范围',
dataIndex: 'applyRange',
key: 'applyRange',
align: 'center',
width: 120
},
{
title: '使用情况',
dataIndex: 'usage',
key: 'usage',
align: 'center',
width: 150
},
{
title: '每人限领',
dataIndex: 'limitPerUser',
key: 'limitPerUser',
align: 'center',
width: 100,
customRender: ({ text }) => (text === -1 ? '无限制' : text)
},
{
title: '状态',
dataIndex: 'isExpire',
key: 'isExpire',
align: 'center',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 120,
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 工具方法
const getCouponTypeText = (type: number) => {
const typeMap = {
10: '满减券',
20: '折扣券',
30: '免费券'
};
return typeMap[type as keyof typeof typeMap] || '未知';
};
const getCouponTypeColor = (type: number) => {
const colorMap = {
10: 'red',
20: 'orange',
30: 'green'
};
return colorMap[type as keyof typeof colorMap] || 'default';
};
const getApplyRangeText = (range: number) => {
const rangeMap = {
10: '全部商品',
20: '指定商品',
30: '指定分类'
};
return rangeMap[range as keyof typeof rangeMap] || '未知';
};
const getApplyRangeColor = (range: number) => {
const colorMap = {
10: 'blue',
20: 'purple',
30: 'cyan'
};
return colorMap[range as keyof typeof colorMap] || 'default';
};
const formatDate = (dateStr: string) => {
return dateStr ? toDateString(dateStr, 'yyyy-MM-dd') : '-';
};
const getUsagePercent = (record: ShopCoupon) => {
if (record.totalCount === -1) return 0;
return Math.round(
((record.issuedCount || 0) / Number(record.totalCount)) * 100
);
};
const getUsageColor = (record: ShopCoupon) => {
const percent = getUsagePercent(record);
if (percent >= 90) return '#ff4d4f';
if (percent >= 70) return '#faad14';
return '#52c41a';
};
/* 搜索 */
const reload = (where?: ShopCouponParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 处理搜索 */
const handleSearch = () => {
reload();
};
/* 重置搜索 */
const handleReset = () => {
Object.assign(searchForm, {
name: '',
type: undefined,
expireType: undefined,
isExpire: undefined
});
reload();
};
/* 清除选择 */
const clearSelection = () => {
selection.value = [];
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopCoupon) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 复制记录 */
const copyRecord = (record: ShopCoupon) => {
const copyData = {
...record,
id: undefined,
name: `${record.name}_副本`,
createTime: undefined,
updateTime: undefined,
issuedCount: 0
};
current.value = copyData;
showEdit.value = true;
message.success('已复制优惠券信息,请修改后保存');
};
/* 删除单个 */
const remove = (row: ShopCoupon) => {
if (row.issuedCount && row.issuedCount > 0) {
message.warning('该优惠券已有用户领取,无法删除');
return;
}
const hide = message.loading('删除中...', 0);
removeShopCoupon(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
// 检查是否有已发放的优惠券
const issuedCoupons = selection.value.filter(
(item) => item.issuedCount && item.issuedCount > 0
);
if (issuedCoupons.length > 0) {
message.warning(
`选中的优惠券中有 ${issuedCoupons.length} 个已被用户领取,无法删除`
);
return;
}
Modal.confirm({
title: '批量删除确认',
content: `确定要删除选中的 ${selection.value.length} 个优惠券吗?此操作不可恢复。`,
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
okText: '确定删除',
okType: 'danger',
cancelText: '取消',
onOk: () => {
const hide = message.loading('批量删除中...', 0);
removeBatchShopCoupon(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
selection.value = [];
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: ShopCoupon) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
},
// 行样式
class: record.isExpire === 1 ? 'expired-row' : ''
};
};
</script>
<script lang="ts">
export default {
name: 'ShopCoupon'
};
</script>
<style lang="less" scoped>
.shop-coupon-container {
.search-container {
background: #fafafa;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
.search-form {
.ant-form-item {
margin-bottom: 8px;
}
}
}
.batch-actions {
margin-bottom: 16px;
}
.coupon-table {
.coupon-name {
text-align: left;
.coupon-description {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
.coupon-value {
.value-amount {
font-size: 16px;
font-weight: bold;
color: #f5222d;
}
.value-discount {
font-size: 16px;
font-weight: bold;
color: #fa8c16;
}
.value-free {
font-size: 14px;
font-weight: bold;
color: #52c41a;
}
.value-condition {
font-size: 12px;
color: #666;
margin-top: 2px;
}
}
.expire-info {
.expire-detail {
font-size: 12px;
color: #666;
margin-top: 4px;
}
}
.usage-info {
.usage-text {
font-size: 12px;
color: #666;
margin-top: 4px;
}
}
.expired-row {
background-color: #fff2f0;
td {
opacity: 0.7;
}
}
}
}
:deep(.ant-table) {
.ant-table-tbody > tr:hover > td {
background-color: #e6f7ff;
}
}
:deep(.ant-progress) {
.ant-progress-text {
font-size: 12px;
}
}
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,158 @@
<!-- 搜索表单 -->
<template>
<div class="search-container">
<!-- 搜索表单 -->
<a-form
:model="searchForm"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="客户名称">
<a-input
v-model:value="searchForm.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 { 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?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerApplyParam): void;
(e: 'add'): 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'
);
}
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', {});
};
</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,645 @@
<!-- 编辑弹窗 -->
<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="客户名称" 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>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
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 { 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 props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerApply | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 跟进记录保存状态
const followUpLoading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 历史跟进记录
const historyRecords = ref<ShopDealerRecord[]>([]);
// 新的跟进内容
const newFollowUpContent = ref('');
// 表单数据
const form = reactive<ShopDealerApply>({
applyId: undefined,
userId: undefined,
nickName: undefined,
realName: '',
mobile: '',
dealerName: '',
rate: 0.007,
refereeId: undefined,
refereeName: 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'
} 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 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 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(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;
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(() => {
loading.value = false;
});
};
watch(
() => props.visible,
async (visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
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();
}
},
{ 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

@@ -0,0 +1,458 @@
<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="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 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>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<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,
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 selection = ref<ShopDealerApply[]>([]);
// 当前编辑数据
const current = ref<ShopDealerApply | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 4;
return pageShopDealerApply({
...where,
...orders,
page,
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 || '-'})`;
}
},
{
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 reload = (where?: ShopDealerApplyParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 审核通过 */
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('审核通过成功');
reload();
} catch (error: any) {
hide();
message.error(error.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 removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
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();
</script>
<script lang="ts">
export default {
name: 'ShopDealerApply'
};
</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,406 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑资金流动记录' : '新增资金流动记录'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<!-- 基本信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">基本信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="分销商用户ID" name="userId">
<a-input-number
:min="1"
placeholder="请输入分销商用户ID"
v-model:value="form.userId"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="订单ID" name="orderId">
<a-input-number
:min="1"
placeholder="请输入订单ID可选"
v-model:value="form.orderId"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 资金流动信息 -->
<a-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-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>
<!-- 金额预览 -->
<div class="amount-preview" v-if="form.money && form.flowType">
<a-alert
:type="getAmountAlertType()"
:message="getAmountPreviewText()"
show-icon
style="margin-top: 16px"
/>
</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 {
addShopDealerCapital,
updateShopDealerCapital
} from '@/api/shop/shopDealerCapital';
import { ShopDealerCapital } from '@/api/shop/shopDealerCapital/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?: ShopDealerCapital | 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<ShopDealerCapital>({
id: undefined,
userId: undefined,
orderId: undefined,
flowType: undefined,
money: undefined,
comments: '',
toUserId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
userId: [
{
required: true,
message: '请输入分销商用户ID',
trigger: 'blur'
}
],
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 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 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);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value
? updateShopDealerCapital
: addShopDealerCapital;
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);
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 {
resetFields();
}
},
{ 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

@@ -0,0 +1,306 @@
<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="id"
: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>
<!-- 编辑弹窗 -->
<ShopDealerCapitalEdit
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 ShopDealerCapitalEdit from './components/shopDealerCapitalEdit.vue';
import {
pageShopDealerCapital,
removeShopDealerCapital,
removeBatchShopDealerCapital
} from '@/api/shop/shopDealerCapital';
import type {
ShopDealerCapital,
ShopDealerCapitalParam
} from '@/api/shop/shopDealerCapital/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerCapital[]>([]);
// 当前编辑数据
const current = ref<ShopDealerCapital | 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 pageShopDealerCapital({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 80,
fixed: 'left'
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 100,
fixed: 'left'
},
{
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: 'left',
width: 200,
ellipsis: true,
customRender: ({ text }) => text || '-'
},
{
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?: ShopDealerCapitalParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerCapital) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerCapital) => {
const hide = message.loading('请求中..', 0);
removeShopDealerCapital(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerCapital(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerCapital) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerCapital'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,89 @@
<!-- 经销商订单导入弹窗 -->
<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

@@ -0,0 +1,182 @@
<template>
<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 { 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';
withDefaults(
defineProps<{
// 选中的数据
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerOrderParam): void;
(e: 'batchSettle'): void;
(e: 'export'): void;
(e: 'importDone'): void;
(e: 'remove'): void;
}>();
// 是否显示导入弹窗
const showImport = ref(false);
// 搜索表单
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

@@ -0,0 +1,363 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '分销订单' : '分销订单'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:okText="`立即结算`"
@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="客户名称" 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 } 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 props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerOrder | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 表单数据
const 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);
};
// 表单验证规则
const rules = reactive({
userId: [
{
required: true,
message: '请选择用户ID',
trigger: 'blur'
}
]
});
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 {
// 重置为默认值
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;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
.dealer-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
border-left: 3px solid #1890ff;
.dealer-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #333;
.ant-tag {
margin-right: 8px;
}
}
}
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
margin: 24px 0 16px 0;
.ant-divider-inner-text {
padding: 0 16px 0 0;
}
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-input-number) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,448 @@
<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="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 }">
<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 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 selection = ref<ShopDealerOrder[]>([]);
// 当前编辑数据
const current = ref<ShopDealerOrder | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 加载状态
const loading = ref(true);
// 当前搜索条件
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
});
};
// 表格列配置
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 batchSettle = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const validOrders = selection.value.filter(
(order) => order.isSettled === 0 && order.isInvalid === 0
);
if (!validOrders.length) {
message.error('所选订单中没有可结算的订单');
return;
}
const totalCommission = validOrders
.reduce((sum, order) => {
return (
sum +
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
);
}, 0)
.toFixed(2);
Modal.confirm({
title: '批量结算确认',
content: `确定要结算选中的 ${validOrders.length} 个订单吗?总佣金金额:¥${totalCommission}`,
icon: createVNode(ExclamationCircleOutlined),
okText: '确认结算',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在批量结算...', 0);
// 这里调用批量结算API
setTimeout(() => {
hide();
message.success(`成功结算 ${validOrders.length} 个订单`);
reload();
}, 1500);
}
});
};
/* 导出数据 */
const 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 removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerOrder(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerOrder) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerOrder'
};
</script>
<style lang="less" scoped>
.order-info {
.order-id {
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.order-price {
color: #ff4d4f;
font-weight: 600;
}
}
.dealer-info {
.dealer-level {
margin-bottom: 6px;
font-size: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
:deep(.detail-section) {
h4 {
color: #1890ff;
margin-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 8px;
}
p {
margin: 4px 0;
line-height: 1.5;
}
}
:deep(.ant-table-tbody > tr > td) {
vertical-align: top;
}
:deep(.ant-tag) {
margin: 2px 4px 2px 0;
}
</style>

View File

@@ -0,0 +1,779 @@
<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,204 @@
<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

@@ -0,0 +1,185 @@
<!-- 搜索表单 -->
<template>
<!-- 搜索表单 -->
<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 { 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?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerRefereeParam): void;
(e: 'add'): void;
(e: 'viewTree'): void;
(e: 'export'): void;
}>();
// 搜索表单
const searchForm = reactive<any>({
dealerId: undefined,
userId: undefined,
level: undefined,
dateRange: undefined
});
// 搜索
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

@@ -0,0 +1,158 @@
<!-- 编辑弹窗 -->
<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="dealerId">
<a-input
allow-clear
placeholder="请输入分销商用户ID"
v-model:value="form.dealerId"
/>
</a-form-item>
<a-form-item label="被推荐人信息" name="userId">
<a-input
allow-clear
placeholder="请输入用户id(被推荐人)"
v-model:value="form.userId"
/>
</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 } 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';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerReferee | 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<ShopDealerReferee>({
id: undefined,
dealerId: undefined,
userId: undefined,
level: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopDealerRefereeName: [
{
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
? updateShopDealerReferee
: addShopDealerReferee;
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,563 @@
<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="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@viewTree="viewRefereeTree"
@export="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<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 === '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)" 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>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<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,
TeamOutlined,
EditOutlined,
ArrowRightOutlined
} 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 ShopDealerRefereeEdit from './components/shopDealerRefereeEdit.vue';
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';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerReferee[]>([]);
// 当前编辑数据
const current = ref<ShopDealerReferee | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示树状图弹窗
const showTree = ref(false);
// 树状图数据
const treeData = ref<ShopDealerReferee[]>([]);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopDealerReferee({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '推荐人信息',
key: 'dealerInfo',
align: 'left',
width: 150
},
{
title: '',
key: 'relationship',
align: 'center',
width: 50
},
{
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: '建立时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 120,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm')
},
{
title: '操作',
key: 'action',
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 = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerReferee) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerReferee) => {
const hide = message.loading('请求中..', 0);
removeShopDealerReferee(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopDealerReferee(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerReferee) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerReferee'
};
</script>
<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

@@ -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,744 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
: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: 4 }"
:wrapper-col="{ span: 20 }"
>
<!-- 基本信息 -->
<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>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
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 { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerSetting | 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 form = reactive<ShopDealerSetting>({
key: undefined,
describe: '',
values: '',
tenantId: undefined,
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);
};
// 表单验证规则
const rules = reactive({
key: [
{
required: true,
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 getTemplateTitle = () => {
const titleMap = {
commission_rate: '佣金比例配置模板',
withdraw_config: '提现配置模板',
level_config: '等级配置模板',
reward_config: '奖励配置模板'
};
return titleMap[form.key] || '配置模板';
};
/* 获取模板描述 */
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);
/* 保存编辑 */
const save = () => {
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,
updateTime: Date.now()
};
const saveOrUpdate = isUpdate.value ? updateShopDealerSetting : addShopDealerSetting;
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) {
jsonStatus.value = null;
if (props.data) {
assignObject(form, props.data);
// 解析配置数据到模板
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 {
resetFields();
}
},
{ 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

@@ -0,0 +1,415 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<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>
<!-- 分销层级 -->
<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 { 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 activeTab = ref('basic');
// 保存状态
const saving = ref(false);
// 基础设置
const basicSettings = reactive({
enableDistribution: true,
distributionLevel: 3,
dealerSelfBuy: false
});
// 分销条件设置
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('加载设置失败');
}
};
/* 保存设置 */
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;
}
};
// 页面加载时获取设置数据
onMounted(() => {
loadSettings();
});
</script>
<script lang="ts">
export default {
name: 'ShopDealerSetting'
};
</script>
<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,89 @@
<!-- 经销商订单导入弹窗 -->
<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

@@ -0,0 +1,101 @@
<template>
<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 { 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';
withDefaults(
defineProps<{
// 选中的数据
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerWithdrawParam): void;
(e: 'batchSettle'): void;
(e: 'export'): void;
(e: 'importDone'): void;
(e: 'remove'): void;
}>();
// 是否显示导入弹窗
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');
};
</script>

View File

@@ -0,0 +1,697 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
: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="备注" 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 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-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-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"
/>
</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 { 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 props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerWithdraw | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
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,
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);
};
// 表单验证规则
const rules = reactive({
userId: [
{
required: true,
message: '请输入分销商用户ID',
trigger: 'blur'
}
],
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: '已打款'
};
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();
}
// 根据支付方式清理不相关字段
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

@@ -0,0 +1,464 @@
<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"
/>
</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 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="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="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>
<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"
/>
</a-page-header>
</template>
<script lang="ts" setup>
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 selection = ref<ShopDealerWithdraw[]>([]);
// 当前编辑数据
const current = ref<ShopDealerWithdraw | 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 pageShopDealerWithdraw({
...where,
...orders,
page,
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}`
};
}
},
{
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 reload = (where?: ShopDealerWithdrawParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 审核通过 */
const approveWithdraw = (row: ShopDealerWithdraw) => {
Modal.confirm({
title: '审核通过确认',
content: `已核对信息进行核对,正确无误!`,
icon: createVNode(CheckOutlined),
okText: '确认通过',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在处理审核...', 0);
// 这里需要调用审核通过的API
setTimeout(() => {
hide();
updateShopDealerWithdraw({
id: row.id,
applyStatus: 20
});
message.success('审核通过成功');
reload();
}, 1000);
}
});
};
/* 审核驳回 */
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 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 query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopDealerWithdraw) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
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,222 @@
<!-- 编辑弹窗 -->
<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,255 @@
<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

@@ -0,0 +1,239 @@
<!-- 编辑弹窗 -->
<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="type">
<a-input allow-clear placeholder="请输入" v-model:value="form.type" />
</a-form-item>
<a-form-item label="" name="title">
<a-input allow-clear placeholder="请输入" v-model:value="form.title" />
</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="状态, 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>
</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 {
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';
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?: ShopExpressTemplate | 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<ShopExpressTemplate>({
id: undefined,
type: undefined,
title: undefined,
firstAmount: undefined,
extraAmount: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
firstNum: undefined,
extraNum: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopExpressTemplateName: [
{
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
? updateShopExpressTemplate
: addShopExpressTemplate;
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,287 @@
<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,236 @@
<!-- 编辑弹窗 -->
<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,299 @@
<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

@@ -0,0 +1,388 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
title="生成礼品卡"
: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="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"
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>
<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';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
}>();
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 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 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);
};
const getGoodsList = async () => {
goodsList.value = await listShopGoods();
};
getGoodsList();
/* 搜索商品 */
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 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

@@ -0,0 +1,621 @@
<!-- 搜索表单 -->
<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-button type="primary" class="ele-btn-icon" @click="openMultiAdd">
<template #icon>
<PlusOutlined />
</template>
<span>批量生成</span>
</a-button>
<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" />
</a-space>
</template>
<script lang="ts" setup>
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 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 }
})
);
// 每行放置3个二维码保持适当间距
const itemsPerRow = 3;
const rows = Math.ceil(qrCodeImages.length / itemsPerRow);
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 qrCodeCells: TableCell[] = [];
const infoCells: TableCell[] = [];
for (let i = 0; i < itemsPerRow; i++) {
if (i < rowItems.length) {
const item = rowItems[i];
// 将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 }
})
);
}
}
// 添加表格
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 }
})
);
}
// 创建文档
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

@@ -0,0 +1,728 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
width="65%"
: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="礼品卡名称" 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 } 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 props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopGift | 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 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
});
// 商品列表
const goodsList = ref<ShopGoods[]>([]);
// 商品加载状态
const goodsLoading = ref(false);
// 选中的商品
const selectedGoods = ref<ShopGoods | null>(null);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
message: '请输入礼品卡名称',
trigger: 'blur'
},
{
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

@@ -0,0 +1,271 @@
<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="id"
: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"
@remove="removeBatch"
@batchMove="openMove"
@done="reload"
/>
</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-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>
<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>
<!-- 编辑弹窗 -->
<ShopGiftEdit 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 type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import { getPageTitle } from '@/utils/common';
import ShopGiftEdit from './components/shopGiftEdit.vue';
import {
pageShopGift,
removeShopGift,
removeBatchShopGift
} from '@/api/shop/shopGift';
import type { ShopGift, ShopGiftParam } from '@/api/shop/shopGift/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopGift[]>([]);
// 当前编辑数据
const current = ref<ShopGift | 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 pageShopGift({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center'
},
{
title: '秘钥',
dataIndex: 'code',
key: 'code',
align: 'center'
},
{
title: '商品',
dataIndex: ['goods', 'name'],
key: 'goodsId',
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 170
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopGiftParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopGift) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopGift) => {
const hide = message.loading('请求中..', 0);
removeShopGift(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchShopGift(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopGift) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopGift'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,38 @@
<!-- 搜索表单 -->
<template>
<a-space
style="flex-wrap: wrap"
v-if="hasRole('superAdmin') || hasRole('admin') || hasRole('foundation')"
/>
</template>
<script lang="ts" setup>
import { watch, nextTick } from 'vue';
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
import { hasRole } from '@/utils/permission';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
const emit = defineEmits<{
(e: 'add'): void;
}>();
nextTick(() => {
if (localStorage.getItem('NotActive')) {
// IsActive.value = false
}
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,158 @@
<!-- 搜索表单 -->
<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-button
danger
type="primary"
class="ele-btn-icon"
:disabled="selection?.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-radio-group v-model:value="type" @change="handleSearch">
<a-radio-button value="出售中"
>出售中({{ goodsCount?.totalNum }})
</a-radio-button>
<a-radio-button value="待上架"
>待上架({{ goodsCount?.totalNum2 }})
</a-radio-button>
<a-radio-button value="已售罄"
>已售罄({{ goodsCount?.totalNum3 }})
</a-radio-button>
</a-radio-group>
<a-tree-select
allow-clear
:tree-data="navigationList"
tree-default-expand-all
style="width: 240px"
:listHeight="700"
placeholder="请选择栏目"
:value="where.categoryId || undefined"
:dropdown-style="{ overflow: 'auto' }"
@update:value="(value?: number) => (where.categoryId = value)"
@change="onCategoryId"
/>
<a-input-search
allow-clear
placeholder="请输入关键词"
style="width: 360px"
v-model:value="where.keywords"
@pressEnter="reload"
@search="reload"
/>
<a-button type="text" @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import { ref, watch } from 'vue';
import { getCount } from '@/api/shop/shopGoods';
import type { GoodsCount, ShopGoodsParam } from '@/api/shop/shopGoods/model';
import useSearch from '@/utils/use-search';
import { getMerchantId } from '@/utils/merchant';
import { CmsNavigation } from '@/api/cms/cmsNavigation/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
merchantId?: number;
navigationList?: CmsNavigation[];
}>(),
{
merchantId: getMerchantId()
}
);
const type = ref<string>();
// 统计数据
const goodsCount = ref<GoodsCount>();
// 表单数据
const { where, resetFields } = useSearch<ShopGoodsParam>({
goodsId: undefined,
isShow: undefined,
status: undefined,
stock: undefined,
categoryId: undefined,
merchantId: undefined,
keywords: ''
});
const emit = defineEmits<{
(e: 'search', where?: ShopGoodsParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
const handleSearch = (e) => {
const text = e.target.value;
resetFields();
if (text === '出售中') {
where.sceneType = 'on_sale';
}
if (text === '待上架') {
where.sceneType = 'pending';
}
if (text === '已售罄') {
where.sceneType = 'sold_out';
}
emit('search', where);
};
const reload = () => {
getCount(where).then((data: any) => {
goodsCount.value = data;
});
emit('search', where);
};
/* 重置 */
const reset = () => {
resetFields();
type.value = '';
reload();
};
// 按分类查询
const onCategoryId = (id: number) => {
where.categoryId = id;
emit('search', where);
};
watch(
() => props.merchantId,
(id) => {
if (Number(id) > 0) {
where.merchantId = id;
reload();
} else {
where.merchantId = undefined;
reload();
}
},
{ immediate: true }
);
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,342 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra />
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="goodsId"
: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"
:navigationList="navigationList"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<a-space class="flex items-center cursor-pointer">
<a-image
:src="record.image"
v-if="record.image"
:preview="false"
:width="50"
/>
<span class="text-gray-700 font-bold">{{ record.name }}</span>
</a-space>
</template>
<template v-if="column.key === 'recommend'">
<a-space @click.stop="onRecommend(record)">
<span
v-if="record.recommend === 1"
class="ele-text-success cursor-pointer"
><CheckOutlined
/></span>
<span v-else class="ele-text-placeholder cursor-pointer"
><CloseOutlined
/></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="orange">待上架</a-tag>
<a-tag v-if="record.status === 2" color="purple">待审核</a-tag>
<a-tag v-if="record.status === 3" color="red">审核不通过</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click.stop="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm.stop="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ShopGoodsEdit
v-model:visible="showEdit"
:navigationList="navigationList"
: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,
CheckOutlined,
CloseOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toTreeData } 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 ShopGoodsEdit from './components/shopGoodsEdit.vue';
import Extra from '@/views/shop/shopGoods/components/extra.vue';
import {
pageShopGoods,
removeShopGoods,
removeBatchShopGoods,
updateShopGoods
} from '@/api/shop/shopGoods';
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model';
import { getPageTitle } from '@/utils/common';
import { CmsNavigation } from '@/api/cms/cmsNavigation/model';
import { listCmsNavigation } from '@/api/cms/cmsNavigation';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopGoods[]>([]);
// 当前编辑数据
const current = ref<ShopGoods | null>(null);
// 栏目数据
const navigationList = ref<CmsNavigation[]>();
// 是否显示编辑弹窗
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 pageShopGoods({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'goodsId',
key: 'goodsId',
align: 'center',
width: 90
},
{
title: '商品',
dataIndex: 'name',
key: 'name',
width: 300
},
// {
// title: '编号',
// dataIndex: 'code',
// key: 'code',
// align: 'center',
// },
{
title: '价格',
dataIndex: 'price',
key: 'price',
align: 'center',
customRender: ({ text }) => `${text}`
},
{
title: '销量',
dataIndex: 'sales',
key: 'sales',
align: 'center'
},
{
title: '库存',
dataIndex: 'stock',
key: 'stock',
align: 'center'
},
{
title: '推荐',
dataIndex: 'recommend',
key: 'recommend',
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center'
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// align: 'center',
// },
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
width: 180,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]);
/* 搜索 */
const reload = (where?: ShopGoodsParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopGoods) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopGoods) => {
const hide = message.loading('请求中..', 0);
removeShopGoods(row.goodsId)
.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);
removeBatchShopGoods(selection.value.map((d) => d.goodsId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
// 加载栏目数据
if (!navigationList.value) {
listCmsNavigation({}).then((res) => {
navigationList.value = toTreeData({
data: res?.map((d) => {
d.value = d.navigationId;
d.label = d.title;
return d;
}),
idField: 'navigationId',
parentIdField: 'parentId'
});
});
}
};
const onRecommend = (row: ShopGoods) => {
updateShopGoods({
...row,
recommend: row.recommend == 1 ? 0 : 1
}).then((msg) => {
message.success(msg);
reload();
});
};
/* 自定义行属性 */
const customRow = (record: ShopGoods) => {
return {
// 行点击事件
onClick: () => {
openEdit(record);
},
// 行双击事件
onDblclick: () => {
// openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopGoods'
};
</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,198 @@
<!-- 编辑弹窗 -->
<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="商品id" name="goodsId">
<a-input
allow-clear
placeholder="请输入商品id"
v-model:value="form.goodsId"
/>
</a-form-item>
<a-form-item label="优惠劵id" name="issueCouponId">
<a-input
allow-clear
placeholder="请输入优惠劵id"
v-model:value="form.issueCouponId"
/>
</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="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="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</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 {
addShopGoodsCoupon,
updateShopGoodsCoupon
} from '@/api/shop/shopGoodsCoupon';
import { ShopGoodsCoupon } from '@/api/shop/shopGoodsCoupon/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?: ShopGoodsCoupon | 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<ShopGoodsCoupon>({
id: undefined,
goodsId: undefined,
issueCouponId: undefined,
deleted: undefined,
userId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopGoodsCouponName: [
{
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
? updateShopGoodsCoupon
: addShopGoodsCoupon;
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,270 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopGoodsCouponId"
: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>
<!-- 编辑弹窗 -->
<ShopGoodsCouponEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
</div>
</div>
</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 ShopGoodsCouponEdit from './components/shopGoodsCouponEdit.vue';
import {
pageShopGoodsCoupon,
removeShopGoodsCoupon,
removeBatchShopGoodsCoupon
} from '@/api/shop/shopGoodsCoupon';
import type {
ShopGoodsCoupon,
ShopGoodsCouponParam
} from '@/api/shop/shopGoodsCoupon/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopGoodsCoupon[]>([]);
// 当前编辑数据
const current = ref<ShopGoodsCoupon | 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 pageShopGoodsCoupon({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '商品id',
dataIndex: 'goodsId',
key: 'goodsId',
align: 'center'
},
{
title: '优惠劵id',
dataIndex: 'issueCouponId',
key: 'issueCouponId',
align: 'center'
},
{
title: '排序(数字越小越靠前)',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center'
},
{
title: '状态, 0正常, 1冻结',
dataIndex: 'status',
key: 'status',
align: 'center'
},
{
title: '是否删除, 0否, 1是',
dataIndex: 'deleted',
key: 'deleted',
align: 'center'
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
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?: ShopGoodsCouponParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopGoodsCoupon) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopGoodsCoupon) => {
const hide = message.loading('请求中..', 0);
removeShopGoodsCoupon(row.shopGoodsCouponId)
.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);
removeBatchShopGoodsCoupon(
selection.value.map((d) => d.shopGoodsCouponId)
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopGoodsCoupon) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopGoodsCoupon'
};
</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,276 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑商品sku列表' : '添加商品sku列表'"
: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="商品ID" name="goodsId">
<a-input
allow-clear
placeholder="请输入商品ID"
v-model:value="form.goodsId"
/>
</a-form-item>
<a-form-item
label="商品属性索引值 (attr_value|attr_value[|....])"
name="sku"
>
<a-input
allow-clear
placeholder="请输入商品属性索引值 (attr_value|attr_value[|....])"
v-model:value="form.sku"
/>
</a-form-item>
<a-form-item label="商品图片" name="image">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="商品价格" name="price">
<a-input
allow-clear
placeholder="请输入商品价格"
v-model:value="form.price"
/>
</a-form-item>
<a-form-item label="市场价格" name="salePrice">
<a-input
allow-clear
placeholder="请输入市场价格"
v-model:value="form.salePrice"
/>
</a-form-item>
<a-form-item label="成本价" name="cost">
<a-input
allow-clear
placeholder="请输入成本价"
v-model:value="form.cost"
/>
</a-form-item>
<a-form-item label="库存" name="stock">
<a-input
allow-clear
placeholder="请输入库存"
v-model:value="form.stock"
/>
</a-form-item>
<a-form-item label="sku编码" name="skuNo">
<a-input
allow-clear
placeholder="请输入sku编码"
v-model:value="form.skuNo"
/>
</a-form-item>
<a-form-item label="商品条码" name="barCode">
<a-input
allow-clear
placeholder="请输入商品条码"
v-model:value="form.barCode"
/>
</a-form-item>
<a-form-item label="重量" name="weight">
<a-input
allow-clear
placeholder="请输入重量"
v-model:value="form.weight"
/>
</a-form-item>
<a-form-item label="体积" name="volume">
<a-input
allow-clear
placeholder="请输入体积"
v-model:value="form.volume"
/>
</a-form-item>
<a-form-item label="唯一值" name="uuid">
<a-input
allow-clear
placeholder="请输入唯一值"
v-model:value="form.uuid"
/>
</a-form-item>
<a-form-item label="状态, 0正常, 1异常" 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>
</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 { addShopGoodsSku, updateShopGoodsSku } from '@/api/shop/shopGoodsSku';
import { ShopGoodsSku } from '@/api/shop/shopGoodsSku/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?: ShopGoodsSku | 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<ShopGoodsSku>({
id: undefined,
goodsId: undefined,
sku: undefined,
image: undefined,
price: undefined,
salePrice: undefined,
cost: undefined,
stock: undefined,
skuNo: undefined,
barCode: undefined,
weight: undefined,
volume: undefined,
uuid: undefined,
status: undefined,
comments: undefined,
tenantId: undefined,
createTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopGoodsSkuName: [
{
required: true,
type: 'string',
message: '请填写商品sku列表名称',
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
? updateShopGoodsSku
: addShopGoodsSku;
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,310 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopGoodsSkuId"
: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>
<!-- 编辑弹窗 -->
<ShopGoodsSkuEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
</div>
</div>
</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 ShopGoodsSkuEdit from './components/shopGoodsSkuEdit.vue';
import {
pageShopGoodsSku,
removeShopGoodsSku,
removeBatchShopGoodsSku
} from '@/api/shop/shopGoodsSku';
import type {
ShopGoodsSku,
ShopGoodsSkuParam
} from '@/api/shop/shopGoodsSku/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopGoodsSku[]>([]);
// 当前编辑数据
const current = ref<ShopGoodsSku | 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 pageShopGoodsSku({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '商品ID',
dataIndex: 'goodsId',
key: 'goodsId',
align: 'center'
},
{
title: '商品属性索引值 (attr_value|attr_value[|....])',
dataIndex: 'sku',
key: 'sku',
align: 'center'
},
{
title: '商品图片',
dataIndex: 'image',
key: 'image',
align: 'center'
},
{
title: '商品价格',
dataIndex: 'price',
key: 'price',
align: 'center'
},
{
title: '市场价格',
dataIndex: 'salePrice',
key: 'salePrice',
align: 'center'
},
{
title: '成本价',
dataIndex: 'cost',
key: 'cost',
align: 'center'
},
{
title: '库存',
dataIndex: 'stock',
key: 'stock',
align: 'center'
},
{
title: 'sku编码',
dataIndex: 'skuNo',
key: 'skuNo',
align: 'center'
},
{
title: '商品条码',
dataIndex: 'barCode',
key: 'barCode',
align: 'center'
},
{
title: '重量',
dataIndex: 'weight',
key: 'weight',
align: 'center'
},
{
title: '体积',
dataIndex: 'volume',
key: 'volume',
align: 'center'
},
{
title: '唯一值',
dataIndex: 'uuid',
key: 'uuid',
align: 'center'
},
{
title: '状态, 0正常, 1异常',
dataIndex: 'status',
key: 'status',
align: 'center'
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopGoodsSkuParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopGoodsSku) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopGoodsSku) => {
const hide = message.loading('请求中..', 0);
removeShopGoodsSku(row.shopGoodsSkuId)
.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);
removeBatchShopGoodsSku(selection.value.map((d) => d.shopGoodsSkuId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopGoodsSku) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopGoodsSku'
};
</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,206 @@
<!-- 编辑弹窗 -->
<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="商品ID" name="goodsId">
<a-input
allow-clear
placeholder="请输入商品ID"
v-model:value="form.goodsId"
/>
</a-form-item>
<a-form-item label="规格ID" name="specId">
<a-input
allow-clear
placeholder="请输入规格ID"
v-model:value="form.specId"
/>
</a-form-item>
<a-form-item label="规格名称" name="specName">
<a-input
allow-clear
placeholder="请输入规格名称"
v-model:value="form.specName"
/>
</a-form-item>
<a-form-item label="规格值" name="specValue">
<a-input
allow-clear
placeholder="请输入规格值"
v-model:value="form.specValue"
/>
</a-form-item>
<a-form-item label="活动类型 0=商品1=秒杀2=砍价3=拼团" name="type">
<a-input
allow-clear
placeholder="请输入活动类型 0=商品1=秒杀2=砍价3=拼团"
v-model:value="form.type"
/>
</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 {
addShopGoodsSpec,
updateShopGoodsSpec
} from '@/api/shop/shopGoodsSpec';
import { ShopGoodsSpec } from '@/api/shop/shopGoodsSpec/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?: ShopGoodsSpec | 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<ShopGoodsSpec>({
id: undefined,
goodsId: undefined,
specId: undefined,
specName: undefined,
specValue: undefined,
type: undefined,
tenantId: undefined,
shopGoodsSpecId: undefined,
shopGoodsSpecName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopGoodsSpecName: [
{
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
? updateShopGoodsSpec
: addShopGoodsSpec;
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,247 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopGoodsSpecId"
: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>
<!-- 编辑弹窗 -->
<ShopGoodsSpecEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
</div>
</div>
</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 ShopGoodsSpecEdit from './components/shopGoodsSpecEdit.vue';
import {
pageShopGoodsSpec,
removeShopGoodsSpec,
removeBatchShopGoodsSpec
} from '@/api/shop/shopGoodsSpec';
import type {
ShopGoodsSpec,
ShopGoodsSpecParam
} from '@/api/shop/shopGoodsSpec/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopGoodsSpec[]>([]);
// 当前编辑数据
const current = ref<ShopGoodsSpec | 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 pageShopGoodsSpec({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '商品ID',
dataIndex: 'goodsId',
key: 'goodsId',
align: 'center'
},
{
title: '规格ID',
dataIndex: 'specId',
key: 'specId',
align: 'center'
},
{
title: '规格名称',
dataIndex: 'specName',
key: 'specName',
align: 'center'
},
{
title: '规格值',
dataIndex: 'specValue',
key: 'specValue',
align: 'center'
},
{
title: '活动类型 0=商品1=秒杀2=砍价3=拼团',
dataIndex: 'type',
key: 'type',
align: 'center'
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopGoodsSpecParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopGoodsSpec) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopGoodsSpec) => {
const hide = message.loading('请求中..', 0);
removeShopGoodsSpec(row.shopGoodsSpecId)
.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);
removeBatchShopGoodsSpec(selection.value.map((d) => d.shopGoodsSpecId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopGoodsSpec) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopGoodsSpec'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,581 @@
<template>
<div class="merchant-apply-container">
<div class="merchant-apply-header">
<h1>商家入驻申请</h1>
<p>欢迎申请成为平台商家请填写以下信息</p>
</div>
<a-steps :current="currentStep" style="margin-bottom: 30px">
<a-step title="基本信息" />
<a-step title="资质信息" />
<a-step title="确认提交" />
</a-steps>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
class="merchant-apply-form"
>
<!-- 第一步基本信息 -->
<div v-show="currentStep === 0">
<a-card title="基本信息" style="margin-bottom: 20px">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="商户名称" name="merchantName">
<a-input
v-model:value="form.merchantName"
placeholder="请输入商户名称"
:maxlength="100"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="证件类型" name="idType">
<a-select
v-model:value="form.idType"
placeholder="请选择证件类型"
>
<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 :span="12">
<a-form-item label="证件号码" name="merchantCode">
<a-input
v-model:value="form.merchantCode"
placeholder="请输入证件号码"
:maxlength="50"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="商户手机号" name="phone">
<a-input
v-model:value="form.phone"
placeholder="请输入联系人手机号"
:maxlength="11"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="商户姓名" name="realName">
<a-input
v-model:value="form.realName"
placeholder="请输入联系人姓名"
:maxlength="20"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证号码" name="idCard">
<a-input
v-model:value="form.idCard"
placeholder="请输入法人身份证号码"
:maxlength="18"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="店铺类型" name="shopType">
<a-input
v-model:value="form.shopType"
placeholder="请输入店铺类型"
:maxlength="50"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="商户分类" name="category">
<a-input
v-model:value="form.category"
placeholder="请输入商户所属分类"
:maxlength="100"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</div>
<!-- 第二步资质信息 -->
<div v-show="currentStep === 1">
<a-card title="资质信息" style="margin-bottom: 20px">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="营业执照" name="yyzz">
<div class="upload-container">
<a-image
v-if="form.yyzz"
:src="form.yyzz"
:width="120"
:height="120"
style="margin-bottom: 10px"
/>
<SelectFile
:placeholder="`请上传营业执照`"
:limit="1"
:data="yyzzImages"
@done="chooseYyzzImage"
@del="onDeleteYyzzImage"
/>
<p class="upload-tip">请上传清晰的营业执照照片</p>
</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证正面" name="sfz1">
<div class="upload-container">
<a-image
v-if="form.sfz1"
:src="form.sfz1"
:width="120"
:height="120"
style="margin-bottom: 10px"
/>
<SelectFile
:placeholder="`请上传身份证正面`"
:limit="1"
:data="sfz1Images"
@done="chooseSfz1Image"
@del="onDeleteSfz1Image"
/>
<p class="upload-tip">请上传清晰的身份证正面照片</p>
</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证反面" name="sfz2">
<div class="upload-container">
<a-image
v-if="form.sfz2"
:src="form.sfz2"
:width="120"
:height="120"
style="margin-bottom: 10px"
/>
<SelectFile
:placeholder="`请上传身份证反面`"
:limit="1"
:data="sfz2Images"
@done="chooseSfz2Image"
@del="onDeleteSfz2Image"
/>
<p class="upload-tip">请上传清晰的身份证反面照片</p>
</div>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="资质图片" name="files">
<SelectFile
:placeholder="`请上传其他资质证明文件`"
:limit="9"
:data="files"
@done="chooseFiles"
@del="onDeleteFiles"
/>
<p class="upload-tip"
>可上传产品合格证授权书等相关资质文件最多9张</p
>
</a-form-item>
</a-col>
</a-row>
</a-card>
</div>
<!-- 第三步确认提交 -->
<div v-show="currentStep === 2">
<a-card title="申请信息确认" style="margin-bottom: 20px">
<a-descriptions
bordered
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
>
<a-descriptions-item label="商户名称">{{
form.merchantName
}}</a-descriptions-item>
<a-descriptions-item label="证件类型">{{
getIdTypeName(form.idType)
}}</a-descriptions-item>
<a-descriptions-item label="证件号码">{{
form.merchantCode
}}</a-descriptions-item>
<a-descriptions-item label="联系人手机号">{{
form.phone
}}</a-descriptions-item>
<a-descriptions-item label="联系人姓名">{{
form.realName
}}</a-descriptions-item>
<a-descriptions-item label="身份证号码">{{
form.idCard
}}</a-descriptions-item>
<a-descriptions-item label="店铺类型">{{
form.shopType
}}</a-descriptions-item>
<a-descriptions-item label="商户分类">{{
form.category
}}</a-descriptions-item>
<a-descriptions-item label="营业执照">
<a-image
v-if="form.yyzz"
:src="form.yyzz"
:width="120"
:height="120"
/>
</a-descriptions-item>
<a-descriptions-item label="身份证正面">
<a-image
v-if="form.sfz1"
:src="form.sfz1"
:width="120"
:height="120"
/>
</a-descriptions-item>
<a-descriptions-item label="身份证反面">
<a-image
v-if="form.sfz2"
:src="form.sfz2"
:width="120"
:height="120"
/>
</a-descriptions-item>
<a-descriptions-item label="其他资质文件">
<div v-if="files.length > 0" class="files-preview">
<a-image
v-for="(file, index) in files"
:key="index"
:src="file.url"
:width="80"
:height="80"
style="margin-right: 10px; margin-bottom: 10px"
/>
</div>
<span v-else></span>
</a-descriptions-item>
</a-descriptions>
<a-alert
message="请仔细核对以上信息提交后将无法修改。审核结果将在3个工作日内通过短信通知您。"
type="info"
show-icon
style="margin-top: 20px"
/>
</a-card>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<a-button
v-if="currentStep > 0"
@click="prevStep"
style="margin-right: 10px"
>
上一步
</a-button>
<a-button v-if="currentStep < 2" type="primary" @click="nextStep">
下一步
</a-button>
<a-button
v-if="currentStep === 2"
type="primary"
@click="submitApply"
:loading="submitLoading"
>
提交申请
</a-button>
</div>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { Form, message } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { addShopMerchantApply } from '@/api/shop/shopMerchantApply';
import { ShopMerchantApply } from '@/api/shop/shopMerchantApply/model';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FileRecord } from '@/api/system/file/model';
const useForm = Form.useForm;
const router = useRouter();
// 当前步骤
const currentStep = ref(0);
// 提交状态
const submitLoading = ref(false);
// 表单引用
const formRef = ref<any>(null);
// 图片数据
const yyzzImages = ref<ItemType[]>([]);
const sfz1Images = ref<ItemType[]>([]);
const sfz2Images = ref<ItemType[]>([]);
const files = ref<ItemType[]>([]);
// 表单数据
const form = reactive<ShopMerchantApply>({
type: 1,
idType: '1', // 默认营业执照
merchantName: undefined,
merchantCode: undefined,
image: undefined,
phone: undefined,
realName: undefined,
idCard: undefined,
shopType: undefined,
category: undefined,
commission: undefined,
keywords: undefined,
yyzz: undefined,
sfz1: undefined,
sfz2: undefined,
files: undefined,
userId: undefined,
ownStore: 0,
recommend: 0,
goodsReview: 1,
name2: undefined,
reason: undefined,
comments: '商家入驻申请',
status: 0,
sortNumber: 100,
tenantId: undefined
});
// 表单验证规则
const rules = reactive({
merchantName: [
{ required: true, message: '请输入商户名称', trigger: 'blur' }
],
idType: [{ required: true, message: '请选择证件类型', trigger: 'change' }],
merchantCode: [
{ required: true, message: '请输入证件号码', trigger: 'blur' }
],
phone: [{ required: true, message: '请输入联系人手机号', trigger: 'blur' }],
realName: [
{ required: true, message: '请输入联系人姓名', trigger: 'blur' }
],
idCard: [{ required: true, message: '请输入身份证号码', trigger: 'blur' }],
shopType: [{ required: true, message: '请输入店铺类型', trigger: 'blur' }],
category: [{ required: true, message: '请输入商户分类', trigger: 'blur' }],
yyzz: [{ required: true, message: '请上传营业执照', trigger: 'change' }],
sfz1: [{ required: true, message: '请上传身份证正面', trigger: 'change' }],
sfz2: [{ required: true, message: '请上传身份证反面', trigger: 'change' }]
});
// 获取证件类型名称
const getIdTypeName = (type: string | undefined) => {
switch (type) {
case '1':
return '营业执照';
case '2':
return '统一社会信用代码';
default:
return '';
}
};
// 图片选择处理
const chooseYyzzImage = (data: FileRecord) => {
yyzzImages.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.yyzz = data.path;
};
const onDeleteYyzzImage = () => {
yyzzImages.value = [];
form.yyzz = undefined;
};
const chooseSfz1Image = (data: FileRecord) => {
sfz1Images.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.sfz1 = data.path;
};
const onDeleteSfz1Image = () => {
sfz1Images.value = [];
form.sfz1 = undefined;
};
const chooseSfz2Image = (data: FileRecord) => {
sfz2Images.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.sfz2 = data.path;
};
const onDeleteSfz2Image = () => {
sfz2Images.value = [];
form.sfz2 = undefined;
};
const chooseFiles = (data: FileRecord) => {
files.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
};
const onDeleteFiles = (index: number) => {
files.value.splice(index, 1);
};
// 步骤控制
const nextStep = () => {
if (!formRef.value) return;
// 验证当前步骤的表单
const validateFields = getValidateFields();
if (validateFields.length > 0) {
formRef.value
.validateFields(validateFields)
.then(() => {
currentStep.value++;
})
.catch(() => {
message.error('请完善必填信息');
});
} else {
currentStep.value++;
}
};
const prevStep = () => {
currentStep.value--;
};
// 获取当前步骤需要验证的字段
const getValidateFields = () => {
switch (currentStep.value) {
case 0:
return [
'merchantName',
'idType',
'merchantCode',
'phone',
'realName',
'idCard',
'shopType',
'category'
];
case 1:
return ['yyzz', 'sfz1', 'sfz2'];
default:
return [];
}
};
// 提交申请
const submitApply = () => {
if (!formRef.value) return;
submitLoading.value = true;
// 处理表单数据
const formData = {
...form,
files:
files.value.length > 0
? JSON.stringify(files.value.map((item) => item.url))
: undefined
};
addShopMerchantApply(formData)
.then((msg) => {
submitLoading.value = false;
message.success('申请提交成功我们将在3个工作日内完成审核');
// 跳转到申请成功页面
router.push('/merchant/success');
})
.catch((e) => {
submitLoading.value = false;
message.error(e.message || '申请提交失败,请稍后重试');
});
};
const { resetFields } = useForm(form, rules);
</script>
<style lang="less" scoped>
.merchant-apply-container {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
background: #fff;
border-radius: 4px;
}
.merchant-apply-header {
text-align: center;
margin-bottom: 30px;
h1 {
font-size: 24px;
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
.merchant-apply-form {
margin-top: 20px;
}
.upload-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.upload-tip {
color: #999;
font-size: 12px;
margin-top: 5px;
}
.files-preview {
display: flex;
flex-wrap: wrap;
}
.form-actions {
text-align: center;
margin-top: 30px;
}
</style>

View File

@@ -0,0 +1,56 @@
<!-- 搜索表单 -->
<template>
<a-space style="flex-wrap: wrap">
<a-button type="primary" @click="openUrl(`/shop/shopMerchantApply`)"
>入驻申请
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { watch, nextTick } from 'vue';
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
import { openUrl } from '@/utils/common';
import { message } from 'ant-design-vue';
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
const emit = defineEmits<{
(e: 'add'): void;
}>();
const add = () => {
emit('add');
};
// 清除缓存
const clearSiteInfoCache = () => {
removeSiteInfoCache(
'SiteInfo:' + localStorage.getItem('TenantId') + '*'
).then((msg) => {
if (msg) {
message.success(msg);
}
});
};
nextTick(() => {
if (localStorage.getItem('NotActive')) {
// IsActive.value = false
}
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,53 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="请输入关键词"
style="width: 280px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import useSearch from '@/utils/use-search';
import { ShopMerchantParam } from '@/api/shop/shopMerchant/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
// 表单数据
const { where } = useSearch<ShopMerchantParam>({
merchantId: undefined,
keywords: ''
});
const emit = defineEmits<{
(e: 'search', where?: any): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
// const add = () => {
// emit('add');
// };
const reload = () => {
emit('search', where);
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,446 @@
<!-- 编辑弹窗 -->
<template>
<a-drawer
width="60%"
: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="merchantName">
<a-input
allow-clear
placeholder="请输入商户名称"
v-model:value="form.merchantName"
/>
</a-form-item>
<a-form-item label="商户编号" name="merchantCode">
<a-input
allow-clear
placeholder="请输入商户编号"
v-model:value="form.merchantCode"
/>
</a-form-item>
<!-- <a-form-item label="商户类型" name="type">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入商户类型"-->
<!-- v-model:value="form.type"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="商户图标" name="image">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</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="realName">
<a-input
allow-clear
placeholder="请输入商户姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="店铺类型" name="shopType">
<a-input
allow-clear
placeholder="请输入店铺类型"
v-model:value="form.shopType"
/>
</a-form-item>
<!-- <a-form-item label="项目分类" name="itemType">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入项目分类"-->
<!-- v-model:value="form.itemType"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="商户分类" name="category">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入商户分类"-->
<!-- v-model:value="form.category"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="商户经营分类" name="merchantCategoryId">
<a-input
allow-clear
placeholder="请输入商户经营分类"
v-model:value="form.merchantCategoryId"
/>
</a-form-item>
<a-form-item label="商户分类" name="merchantCategoryTitle">
<a-input
allow-clear
placeholder="请输入商户分类"
v-model:value="form.merchantCategoryTitle"
/>
</a-form-item>
<a-form-item label="经纬度" name="lngAndLat">
<a-input
allow-clear
placeholder="请输入经纬度"
v-model:value="form.lngAndLat"
/>
</a-form-item>
<!-- <a-form-item label="" name="lng">-->
<!-- <a-input allow-clear placeholder="请输入" v-model:value="form.lng" />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="" name="lat">-->
<!-- <a-input allow-clear placeholder="请输入" v-model:value="form.lat" />-->
<!-- </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="region">
<a-input
allow-clear
placeholder="请输入所在辖区"
v-model:value="form.region"
/>
</a-form-item>
<a-form-item label="详细地址" name="address">
<a-input
allow-clear
placeholder="请输入详细地址"
v-model:value="form.address"
/>
</a-form-item>
<a-form-item label="手续费" name="commission">
<a-input
allow-clear
placeholder="请输入手续费"
v-model:value="form.commission"
/>
</a-form-item>
<a-form-item label="关键字" name="keywords">
<a-input
allow-clear
placeholder="请输入关键字"
v-model:value="form.keywords"
/>
</a-form-item>
<a-form-item label="资质图片" name="files">
<SelectFile
:placeholder="`请选择图片`"
:limit="9"
:data="files"
@done="chooseFiles"
@del="onDeleteFiles"
/>
</a-form-item>
<a-form-item label="营业时间" name="businessTime">
<a-input
allow-clear
placeholder="请输入营业时间"
v-model:value="form.businessTime"
/>
</a-form-item>
<a-form-item label="商户介绍" name="content">
<a-input
allow-clear
placeholder="请输入商户介绍"
v-model:value="form.content"
/>
</a-form-item>
<a-form-item label="每小时价格" name="price">
<a-input
allow-clear
placeholder="请输入每小时价格"
v-model:value="form.price"
/>
</a-form-item>
<a-form-item label="是否自营" name="ownStore">
<a-input
allow-clear
placeholder="请输入是否自营"
v-model:value="form.ownStore"
/>
</a-form-item>
<a-form-item label="是否可以快递" name="canExpress">
<a-input
allow-clear
placeholder="请输入是否可以快递"
v-model:value="form.canExpress"
/>
</a-form-item>
<a-form-item label="是否推荐" name="recommend">
<a-input
allow-clear
placeholder="请输入是否推荐"
v-model:value="form.recommend"
/>
</a-form-item>
<a-form-item label="是否营业" name="isOn">
<a-input
allow-clear
placeholder="请输入是否营业"
v-model:value="form.isOn"
/>
</a-form-item>
<a-form-item label="开始时间" name="startTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.startTime"
/>
</a-form-item>
<a-form-item label="结束时间" name="endTime">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.endTime"
/>
</a-form-item>
<a-form-item label="是否需要审核" name="goodsReview">
<a-input
allow-clear
placeholder="请输入是否需要审核"
v-model:value="form.goodsReview"
/>
</a-form-item>
<a-form-item label="管理入口" name="adminUrl">
<a-input
allow-clear
placeholder="请输入管理入口"
v-model:value="form.adminUrl"
/>
</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="userId">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入所有人"-->
<!-- v-model:value="form.userId"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="状态" 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="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-form>
</a-drawer>
</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 { addShopMerchant, updateShopMerchant } from '@/api/shop/shopMerchant';
import { ShopMerchant } from '@/api/shop/shopMerchant/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 files = ref<ItemType[]>([]);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopMerchant | 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<ShopMerchant>({
merchantId: undefined,
merchantName: undefined,
merchantCode: undefined,
type: undefined,
image: undefined,
phone: undefined,
realName: undefined,
shopType: undefined,
itemType: undefined,
category: undefined,
merchantCategoryId: undefined,
merchantCategoryTitle: undefined,
lngAndLat: undefined,
lng: undefined,
lat: undefined,
province: undefined,
city: undefined,
region: undefined,
address: undefined,
commission: undefined,
keywords: undefined,
files: undefined,
businessTime: undefined,
content: undefined,
price: undefined,
ownStore: undefined,
canExpress: undefined,
recommend: undefined,
isOn: undefined,
startTime: undefined,
endTime: undefined,
goodsReview: undefined,
adminUrl: undefined,
comments: undefined,
userId: undefined,
deleted: undefined,
status: undefined,
sortNumber: 100,
tenantId: undefined,
createTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopMerchantName: [
{
required: true,
type: 'string',
message: '请填写商户名称',
trigger: 'blur'
}
]
});
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const chooseFiles = (data: FileRecord) => {
files.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
};
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
? updateShopMerchant
: addShopMerchant;
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,277 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra />
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="merchantId"
: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 === 'merchantName'">
<a-space>
<a-image :src="record.image" :width="50" :preview="false" />
<div class="flex-col">
<div class="font-bold">{{ record.merchantName }}</div>
</div>
</a-space>
</template>
<template v-if="column.key === 'userId'">
<div class="flex-col">
<div>{{ record.realName }}</div>
<div class="text-gray-400">{{ record.phone }}</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>
<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>
<!-- 编辑弹窗 -->
<ShopMerchantEdit
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 ShopMerchantEdit from './components/shopMerchantEdit.vue';
import {
pageShopMerchant,
removeShopMerchant,
removeBatchShopMerchant
} from '@/api/shop/shopMerchant';
import type {
ShopMerchant,
ShopMerchantParam
} from '@/api/shop/shopMerchant/model';
import Extra from './components/extra.vue';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopMerchant[]>([]);
// 当前编辑数据
const current = ref<ShopMerchant | 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 pageShopMerchant({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'merchantId',
key: 'merchantId',
width: 90
},
{
title: '商户名称',
dataIndex: 'merchantName',
key: 'merchantName'
},
{
title: '用户信息',
dataIndex: 'userId',
key: 'userId'
},
{
title: '店铺类型',
dataIndex: 'shopType',
key: 'shopType',
align: 'center',
width: 120
},
// {
// title: '商户分类',
// dataIndex: 'category',
// key: 'category',
// align: 'center'
// },
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 120
},
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
width: 180,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopMerchantParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopMerchant) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopMerchant) => {
const hide = message.loading('请求中..', 0);
removeShopMerchant(row.shopMerchantId)
.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);
removeBatchShopMerchant(selection.value.map((d) => d.merchantId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopMerchant) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopMerchant'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,375 @@
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
title="审核商户入驻申请"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="submitReview"
>
<a-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="申请信息">
<a-descriptions
bordered
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
>
<a-descriptions-item label="商户名称">{{
data?.merchantName
}}</a-descriptions-item>
<a-descriptions-item label="证件号码">{{
data?.merchantCode
}}</a-descriptions-item>
<a-descriptions-item label="联系人手机号">{{
data?.phone
}}</a-descriptions-item>
<a-descriptions-item label="联系人姓名">{{
data?.realName
}}</a-descriptions-item>
<a-descriptions-item label="身份证号码">{{
data?.idCard
}}</a-descriptions-item>
<a-descriptions-item label="店铺类型">{{
data?.shopType
}}</a-descriptions-item>
<a-descriptions-item label="商户分类">{{
data?.category
}}</a-descriptions-item>
<a-descriptions-item label="手续费"
>{{ data?.commission }}%</a-descriptions-item
>
<a-descriptions-item label="创建时间">{{
data?.createTime
}}</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="2" tab="资质文件">
<a-row :gutter="24">
<a-col :span="8">
<div class="file-preview">
<h4>营业执照</h4>
<a-image
v-if="data?.yyzz"
:src="data?.yyzz"
:width="150"
:height="150"
/>
<a-empty v-else description="未上传" />
</div>
</a-col>
<a-col :span="8">
<div class="file-preview">
<h4>身份证正面</h4>
<a-image
v-if="data?.sfz1"
:src="data?.sfz1"
:width="150"
:height="150"
/>
<a-empty v-else description="未上传" />
</div>
</a-col>
<a-col :span="8">
<div class="file-preview">
<h4>身份证反面</h4>
<a-image
v-if="data?.sfz2"
:src="data?.sfz2"
:width="150"
:height="150"
/>
<a-empty v-else description="未上传" />
</div>
</a-col>
<a-col :span="24" style="margin-top: 20px">
<div class="file-preview">
<h4>其他资质文件</h4>
<div v-if="otherFiles.length > 0" class="files-grid">
<a-image
v-for="(file, index) in otherFiles"
:key="index"
:src="file"
:width="100"
:height="100"
style="margin-right: 10px; margin-bottom: 10px"
/>
</div>
<a-empty v-else description="未上传其他资质文件" />
</div>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="3" tab="审核处理">
<a-form
ref="formRef"
:model="reviewForm"
:rules="reviewRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="审核结果" name="status" required>
<a-radio-group v-model:value="reviewForm.status">
<a-radio :value="1">通过</a-radio>
<a-radio :value="2">驳回</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="reviewForm.status === 2"
label="驳回原因"
name="reason"
required
>
<a-textarea
v-model:value="reviewForm.reason"
:rows="4"
:maxlength="200"
placeholder="请输入驳回原因"
/>
</a-form-item>
<a-form-item
v-if="reviewForm.status === 1"
label="手续费(%)"
name="commission"
>
<a-input-number
v-model:value="reviewForm.commission"
:step="0.1"
:max="100"
:min="0"
:precision="2"
placeholder="请输入手续费"
/>
</a-form-item>
<a-form-item
v-if="reviewForm.status === 1"
label="是否自营"
name="ownStore"
>
<a-switch
checked-children=""
un-checked-children=""
:checked="reviewForm.ownStore === 1"
@update:checked="(val) => (reviewForm.ownStore = val ? 1 : 0)"
/>
</a-form-item>
<a-form-item
v-if="reviewForm.status === 1"
label="是否推荐"
name="recommend"
>
<a-switch
checked-children=""
un-checked-children=""
:checked="reviewForm.recommend === 1"
@update:checked="(val) => (reviewForm.recommend = val ? 1 : 0)"
/>
</a-form-item>
<a-form-item
v-if="reviewForm.status === 1"
label="是否需要审核"
name="goodsReview"
>
<a-switch
checked-children=""
un-checked-children=""
:checked="reviewForm.goodsReview === 1"
@update:checked="(val) => (reviewForm.goodsReview = val ? 1 : 0)"
/>
</a-form-item>
<a-form-item
v-if="reviewForm.status === 1"
label="创建商户"
name="createMerchant"
>
<a-switch
checked-children=""
un-checked-children=""
:checked="reviewForm.createMerchant"
@update:checked="(val) => (reviewForm.createMerchant = val)"
/>
<div class="tips">审核通过后是否立即创建商户</div>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
v-model:value="reviewForm.comments"
:rows="4"
:maxlength="200"
placeholder="请输入备注信息"
/>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import {
checkShopMerchantApply,
createMerchantFromApply
} from '@/api/shop/shopMerchantApply';
import { ShopMerchantApply } from '@/api/shop/shopMerchantApply/model';
const useForm = Form.useForm;
const props = defineProps<{
visible: boolean;
data?: ShopMerchantApply | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表单引用
const formRef = ref<any>(null);
// 当前标签页
const activeKey = ref('1');
// 其他资质文件
const otherFiles = ref<string[]>([]);
// 审核表单
const reviewForm = reactive({
status: 1, // 默认通过
reason: '',
commission: 0,
ownStore: 0,
recommend: 0,
goodsReview: 1,
createMerchant: true, // 默认创建商户
comments: ''
});
// 审核表单验证规则
const reviewRules = reactive({
status: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
reason: [{ required: true, message: '请输入驳回原因', trigger: 'blur' }]
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 提交审核
const submitReview = () => {
if (!formRef.value) return;
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
applyId: props.data?.applyId,
status: reviewForm.status,
reason: reviewForm.reason,
commission: reviewForm.commission,
ownStore: reviewForm.ownStore,
recommend: reviewForm.recommend,
goodsReview: reviewForm.goodsReview,
comments: reviewForm.comments
};
checkShopMerchantApply(formData)
.then(async (msg) => {
message.success(msg);
// 如果审核通过且需要创建商户
if (
reviewForm.status === 1 &&
reviewForm.createMerchant &&
props.data?.applyId
) {
try {
await createMerchantFromApply(props.data.applyId);
message.success('商户创建成功');
} catch (e) {
message.error('商户创建失败: ' + (e as Error).message);
}
}
loading.value = false;
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const { resetFields } = useForm(reviewForm, reviewRules);
watch(
() => props.visible,
(visible) => {
if (visible) {
resetFields();
reviewForm.status = 1;
reviewForm.createMerchant = true;
// 解析其他资质文件
otherFiles.value = [];
if (props.data?.files) {
try {
const files = JSON.parse(props.data.files);
if (Array.isArray(files)) {
otherFiles.value = files;
} else {
otherFiles.value = [props.data.files];
}
} catch {
otherFiles.value = [props.data.files];
}
}
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
.file-preview {
text-align: center;
h4 {
margin-bottom: 10px;
font-weight: normal;
}
}
.files-grid {
display: flex;
flex-wrap: wrap;
}
.tips {
font-size: 12px;
color: #999;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<!-- 搜索表单 -->
<a-form
:model="searchForm"
layout="inline"
class="search-form"
@finish="handleSearch"
>
<a-form-item label="商户名称">
<a-input
v-model:value="searchForm.merchantName"
placeholder="请输入商户名称"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="联系人">
<a-input
v-model:value="searchForm.realName"
placeholder="请输入联系人"
allow-clear
style="width: 160px"
:maxlength="20"
/>
</a-form-item>
<a-form-item label="联系电话">
<a-input
v-model:value="searchForm.phone"
placeholder="请输入联系电话"
allow-clear
style="width: 160px"
:maxlength="11"
/>
</a-form-item>
<a-form-item label="店铺类型">
<a-input
v-model:value="searchForm.shopType"
placeholder="请输入店铺类型"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="0">待审核</a-select-option>
<a-select-option :value="1">审核通过</a-select-option>
<a-select-option :value="2">审核驳回</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetForm">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
<a-button type="primary" @click="emit('add')">
<template #icon>
<PlusOutlined />
</template>
新增
</a-button>
<a-dropdown v-if="selection.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="emit('remove', selection)">
<DeleteOutlined />
批量删除
</a-menu-item>
</a-menu>
</template>
<a-button>
批量操作
<DownOutlined />
</a-button>
</a-dropdown>
</a-space>
</a-form-item>
</a-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
DeleteOutlined,
DownOutlined
} from '@ant-design/icons-vue';
const emit = defineEmits(['search', 'add', 'remove']);
// 搜索表单数据
const searchForm = reactive({
merchantName: '',
realName: '',
phone: '',
shopType: '',
status: undefined
});
// 搜索事件
const handleSearch = () => {
emit('search', { ...searchForm });
};
// 重置表单
const resetForm = () => {
Object.assign(searchForm, {
merchantName: '',
realName: '',
phone: '',
shopType: '',
status: undefined
});
emit('search', { ...searchForm });
};
defineProps({
selection: {
type: Array,
default: () => []
}
});
</script>

View File

@@ -0,0 +1,673 @@
<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-tabs v-model:activeKey="activeKey">
<a-tab-pane key="1" tab="基本信息">
<a-form-item label="商户名称" name="merchantName">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入商户名称"
v-model:value="form.merchantName"
/>
</a-form-item>
<a-form-item label="证件号码" name="merchantCode">
<a-input
allow-clear
:maxlength="50"
placeholder="请输入证件号码"
v-model:value="form.merchantCode"
/>
</a-form-item>
<a-form-item label="商户图标" name="image">
<div class="flex items-center">
<a-image
v-if="form.image"
:src="form.image"
:width="80"
:height="80"
style="margin-right: 10px"
/>
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteImage"
/>
</div>
</a-form-item>
<a-form-item
label="商户手机号"
name="phone"
extra="手机号码将用做于商户端的登录账号请填写真实手机号码"
>
<a-input
allow-clear
:maxlength="11"
placeholder="请输入商户手机号"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="商户姓名" name="realName">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入商户姓名"
v-model:value="form.realName"
/>
</a-form-item>
<a-form-item label="身份证号码" name="idCard">
<a-input
allow-clear
:maxlength="18"
placeholder="请输入身份证号码"
v-model:value="form.idCard"
/>
</a-form-item>
<a-form-item label="店铺类型" name="shopType">
<a-input
allow-clear
:maxlength="50"
placeholder="请输入店铺类型"
v-model:value="form.shopType"
/>
</a-form-item>
<a-form-item label="商户分类" name="category">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入商户分类"
v-model:value="form.category"
/>
</a-form-item>
</a-tab-pane>
<a-tab-pane key="2" tab="资质信息">
<a-form-item label="营业执照" name="yyzz">
<div class="flex items-center">
<a-image
v-if="form.yyzz"
:src="form.yyzz"
:width="80"
:height="80"
style="margin-right: 10px"
/>
<SelectFile
:placeholder="`请选择营业执照`"
:limit="1"
:data="yyzzImages"
@done="chooseYyzzImage"
@del="onDeleteYyzzImage"
/>
</div>
</a-form-item>
<a-form-item label="身份证正面" name="sfz1">
<div class="flex items-center">
<a-image
v-if="form.sfz1"
:src="form.sfz1"
:width="80"
:height="80"
style="margin-right: 10px"
/>
<SelectFile
:placeholder="`请选择身份证正面`"
:limit="1"
:data="sfz1Images"
@done="chooseSfz1Image"
@del="onDeleteSfz1Image"
/>
</div>
</a-form-item>
<a-form-item label="身份证反面" name="sfz2">
<div class="flex items-center">
<a-image
v-if="form.sfz2"
:src="form.sfz2"
:width="80"
:height="80"
style="margin-right: 10px"
/>
<SelectFile
:placeholder="`请选择身份证反面`"
:limit="1"
:data="sfz2Images"
@done="chooseSfz2Image"
@del="onDeleteSfz2Image"
/>
</div>
</a-form-item>
<a-form-item label="资质图片" name="files">
<SelectFile
:placeholder="`请选择资质图片`"
:limit="9"
:data="files"
@done="chooseFiles"
@del="onDeleteFiles"
/>
</a-form-item>
</a-tab-pane>
<a-tab-pane key="3" tab="其他信息">
<a-form-item label="手续费(%)" name="commission">
<a-input-number
:step="0.1"
:max="100"
:min="0"
:precision="2"
class="ele-fluid"
placeholder="请输入手续费"
v-model:value="form.commission"
/>
</a-form-item>
<a-form-item label="关键字" name="keywords">
<a-select
v-model:value="form.keywords"
mode="tags"
placeholder="输入关键词后回车"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="是否自营" name="ownStore">
<a-switch
checked-children=""
un-checked-children=""
:checked="form.ownStore === 1"
@update:checked="updateOwnStore"
/>
</a-form-item>
<a-form-item label="是否推荐" name="recommend">
<a-switch
checked-children=""
un-checked-children=""
:checked="form.recommend === 1"
@update:checked="updateRecommend"
/>
</a-form-item>
<a-form-item label="是否需要审核" name="goodsReview">
<a-switch
checked-children=""
un-checked-children=""
:checked="form.goodsReview === 1"
@update:checked="updateGoodsReview"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">待审核</a-radio>
<a-radio :value="1">审核通过</a-radio>
<a-radio :value="2">审核驳回</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="驳回原因" name="reason" v-if="form.status === 2">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入驳回原因"
v-model:value="form.reason"
/>
</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-tab-pane>
</a-tabs>
</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 {
addShopMerchantApply,
updateShopMerchantApply
} from '@/api/shop/shopMerchantApply';
import { ShopMerchantApply } from '@/api/shop/shopMerchantApply/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?: ShopMerchantApply | 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 activeKey = ref('1');
// 图片数据
const images = ref<ItemType[]>([]);
const yyzzImages = ref<ItemType[]>([]);
const sfz1Images = ref<ItemType[]>([]);
const sfz2Images = ref<ItemType[]>([]);
const files = ref<ItemType[]>([]);
// 表单数据
const form = reactive<ShopMerchantApply>({
applyId: undefined,
type: 1,
merchantName: undefined,
merchantCode: undefined,
image: undefined,
phone: undefined,
realName: undefined,
idCard: undefined,
shopType: undefined,
category: undefined,
commission: undefined,
keywords: undefined,
yyzz: undefined,
sfz1: undefined,
sfz2: undefined,
files: undefined,
userId: undefined,
ownStore: 0,
recommend: 0,
goodsReview: 1,
name2: undefined,
reason: undefined,
comments: undefined,
status: 0,
sortNumber: 100,
tenantId: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
merchantName: [
{
required: true,
type: 'string',
message: '请输入商户名称',
trigger: 'blur'
}
],
merchantCode: [
{
required: true,
type: 'string',
message: '请输入证件号码',
trigger: 'blur'
}
],
phone: [
{
required: true,
message: '请输入手机号',
trigger: 'blur'
}
],
realName: [
{
required: true,
type: 'string',
message: '请输入商户姓名',
trigger: 'blur'
}
],
idCard: [
{
required: true,
type: 'string',
message: '请输入身份证号码',
trigger: 'blur'
}
],
shopType: [
{
required: true,
type: 'string',
message: '请输入店铺类型',
trigger: 'blur'
}
],
category: [
{
required: true,
type: 'string',
message: '请输入商户分类',
trigger: 'blur'
}
],
yyzz: [
{
required: true,
message: '请上传营业执照',
trigger: 'change'
}
],
sfz1: [
{
required: true,
message: '请上传身份证正面',
trigger: 'change'
}
],
sfz2: [
{
required: true,
message: '请上传身份证反面',
trigger: 'change'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
// 处理关键字数组转字符串
let keywordsStr = '';
if (Array.isArray(form.keywords)) {
keywordsStr = JSON.stringify(form.keywords);
} else if (typeof form.keywords === 'string') {
try {
JSON.parse(form.keywords);
keywordsStr = form.keywords;
} catch {
keywordsStr = JSON.stringify([form.keywords]);
}
}
const formData = {
...form,
keywords: keywordsStr,
files:
files.value.length > 0
? JSON.stringify(files.value.map((item) => item.url))
: undefined
};
const saveOrUpdate = isUpdate.value
? updateShopMerchantApply
: addShopMerchantApply;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// 图片选择处理
const chooseImage = (data: FileRecord) => {
images.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.image = data.path;
};
const onDeleteImage = () => {
images.value = [];
form.image = undefined;
};
const chooseYyzzImage = (data: FileRecord) => {
yyzzImages.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.yyzz = data.path;
};
const onDeleteYyzzImage = () => {
yyzzImages.value = [];
form.yyzz = undefined;
};
const chooseSfz1Image = (data: FileRecord) => {
sfz1Images.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.sfz1 = data.path;
};
const onDeleteSfz1Image = () => {
sfz1Images.value = [];
form.sfz1 = undefined;
};
const chooseSfz2Image = (data: FileRecord) => {
sfz2Images.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.sfz2 = data.path;
};
const onDeleteSfz2Image = () => {
sfz2Images.value = [];
form.sfz2 = undefined;
};
const chooseFiles = (data: FileRecord) => {
files.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
};
const onDeleteFiles = (index: number) => {
files.value.splice(index, 1);
};
// Switch开关处理
const updateOwnStore = (value: boolean) => {
form.ownStore = value ? 1 : 0;
};
const updateRecommend = (value: boolean) => {
form.recommend = value ? 1 : 0;
};
const updateGoodsReview = (value: boolean) => {
form.goodsReview = value ? 1 : 0;
};
const { resetFields } = useForm(form, rules);
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
yyzzImages.value = [];
sfz1Images.value = [];
sfz2Images.value = [];
files.value = [];
if (props.data) {
isUpdate.value = true;
assignObject(form, props.data);
// 处理图片回显
if (props.data.image) {
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
});
}
if (props.data.yyzz) {
yyzzImages.value.push({
uid: uuid(),
url: props.data.yyzz,
status: 'done'
});
}
if (props.data.sfz1) {
sfz1Images.value.push({
uid: uuid(),
url: props.data.sfz1,
status: 'done'
});
}
if (props.data.sfz2) {
sfz2Images.value.push({
uid: uuid(),
url: props.data.sfz2,
status: 'done'
});
}
// 处理关键字回显
if (props.data.keywords) {
try {
form.keywords = JSON.parse(props.data.keywords);
} catch {
form.keywords = [props.data.keywords];
}
}
// 处理资质图片回显
if (props.data.files) {
try {
const fileUrls = JSON.parse(props.data.files);
if (Array.isArray(fileUrls)) {
fileUrls.forEach((url) => {
files.value.push({
uid: uuid(),
url: url,
status: 'done'
});
});
}
} catch {
// 如果解析失败当作单个URL处理
files.value.push({
uid: uuid(),
url: props.data.files,
status: 'done'
});
}
}
} else {
isUpdate.value = false;
// 设置默认值
form.ownStore = 0;
form.recommend = 0;
form.goodsReview = 1;
form.status = 0;
form.sortNumber = 100;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.ele-fluid {
width: 100%;
}
</style>

View File

@@ -0,0 +1,211 @@
<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="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"
@remove="removeBatch"
/>
</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="blue">待审核</a-tag>
<a-tag v-if="record.status === 1" color="green">审核通过</a-tag>
<a-tag v-if="record.status === 2" color="red">审核驳回</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">详情</a>
<a-divider type="vertical" />
<a v-if="record.status === 0" @click="openReview(record)">审核</a>
<a-divider v-if="record.status === 0" 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-page-header>
<!-- 编辑弹窗 -->
<ShopMerchantApplyEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
<!-- 审核弹窗 -->
<ApplyReview v-model:visible="showReview" :data="current" @done="reload" />
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import ShopMerchantApplyEdit from './components/shopMerchantApplyEdit.vue';
import ApplyReview from './components/applyReview.vue';
import Search from './components/search.vue';
import {
pageShopMerchantApply,
removeShopMerchantApply,
removeBatchShopMerchantApply
} from '@/api/shop/shopMerchantApply';
// 页面标题
const getPageTitle = () => {
return '商户入驻申请';
};
const route = useRoute();
const tableRef = ref();
// 表格列配置
const columns = reactive([
{
title: 'ID',
dataIndex: 'applyId',
width: 80,
align: 'center'
},
{
title: '商户名称',
dataIndex: 'merchantName',
ellipsis: true
},
{
title: '联系人',
dataIndex: 'realName'
},
{
title: '联系电话',
dataIndex: 'phone'
},
{
title: '店铺类型',
dataIndex: 'shopType'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180
},
{
title: '操作',
key: 'action',
align: 'center',
width: 150
}
]);
// 表格数据源
const datasource = ({ page, limit, where }) => {
return pageShopMerchantApply({ ...where, page, limit });
};
// 当前选中数据
const current = ref(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示审核弹窗
const showReview = ref(false);
// 表格选中项
const selection = ref([]);
// 刷新表格
const reload = (where) => {
tableRef.value?.reload({ where });
};
// 打开编辑弹窗
const openEdit = (record) => {
current.value = record;
showEdit.value = true;
};
// 打开审核弹窗
const openReview = (record) => {
current.value = record;
showReview.value = true;
};
// 删除单条记录
const remove = (record) => {
removeShopMerchantApply(record.applyId)
.then((msg) => {
message.success(msg);
reload();
})
.catch((e) => {
message.error(e.message);
});
};
// 批量删除
const removeBatch = (records) => {
const ids = records.map((item) => item.applyId);
removeBatchShopMerchantApply(ids)
.then((msg) => {
message.success(msg);
reload();
})
.catch((e) => {
message.error(e.message);
});
};
// 自定义行属性
const customRow = (record) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
</script>
<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;
}
}
</style>

View File

@@ -0,0 +1,411 @@
<!-- 订单发货弹窗 -->
<template>
<a-modal
:visible="visible"
title="订单发送货"
width="600px"
:confirm-loading="loading"
@update:visible="updateVisible"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<!-- 配送方式 -->
<a-form-item label="配送方式" name="deliveryType">
<a-radio-group v-model:value="form.deliveryType">
<a-radio :value="0">
<span style="color: #1890ff">快递配送</span>
</a-radio>
<a-radio :value="1">无需发货</a-radio>
<a-radio :value="2">商家送货</a-radio>
</a-radio-group>
</a-form-item>
<!-- 发货类型 -->
<a-form-item
v-if="form.deliveryType === 0"
label="发货类型"
name="deliveryMethod"
>
<a-radio-group v-model:value="form.deliveryMethod">
<a-radio value="manual">
<span style="color: #1890ff">手动填写</span>
</a-radio>
<a-radio value="print">电子面单打印</a-radio>
</a-radio-group>
</a-form-item>
<!-- 快递公司 -->
<a-form-item
label="快递公司"
name="expressId"
v-if="form.deliveryType === 0"
>
<a-row :gutter="8">
<a-col :span="18">
<a-select
v-model:value="form.expressId"
placeholder="请选择"
show-search
:filter-option="filterExpressOption"
@change="onExpressChange"
>
<a-select-option
v-for="express in expressList"
:key="express.expressId"
:value="express.expressId"
>
{{ express.expressName }}
</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-button type="primary" @click="openExpressModal">
设置物流公司
</a-button>
</a-col>
</a-row>
</a-form-item>
<template v-if="form.deliveryType !== 1">
<a-form-item label="发货人" name="sendName">
<a-input
v-model:value="form.sendName"
placeholder="请输入发货人"
:maxlength="50"
/>
</a-form-item>
<a-form-item label="发货人联系方式" name="sendPhone">
<a-input
v-model:value="form.sendPhone"
placeholder="请输入发货人联系方式"
:maxlength="50"
/>
</a-form-item>
<a-form-item label="发货地址" name="sendAddress">
<a-input
v-model:value="form.sendAddress"
placeholder="请输入发货地址"
/>
</a-form-item>
</template>
<!-- 快递单号 -->
<!-- <a-form-item-->
<!-- label="快递单号"-->
<!-- name="trackingNumber"-->
<!-- v-if="form.deliveryType === 0"-->
<!-- >-->
<!-- <a-input-->
<!-- v-model:value="form.trackingNumber"-->
<!-- placeholder="请输入快递单号"-->
<!-- :maxlength="50"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- &lt;!&ndash; 分单发货 &ndash;&gt;-->
<!-- <a-form-item label="分单发货" v-if="form.deliveryType === 0">-->
<!-- <a-switch-->
<!-- v-model:checked="form.partialDelivery"-->
<!-- checked-children="支持"-->
<!-- un-checked-children="不支持"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- 发货备注 -->
<a-form-item label="发货备注" name="deliveryNote">
<a-textarea
v-model:value="form.deliveryNote"
placeholder="请输入发货备注(可选)"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
<!-- 发货时间 -->
<a-form-item label="发货时间" name="deliveryTime">
<a-date-picker
v-model:value="form.deliveryTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择发货时间"
style="width: 100%"
/>
</a-form-item>
</a-form>
<!-- 快递公司设置弹窗 -->
<express-setting-modal
v-model:visible="expressModalVisible"
@done="loadExpressList"
/>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { ShopOrder } from '@/api/shop/shopOrder/model';
import { updateShopOrder } from '@/api/shop/shopOrder';
import { listShopExpress } from '@/api/shop/shopExpress';
import { ShopExpress } from '@/api/shop/shopExpress/model';
import { toDateString } from 'ele-admin-pro';
import dayjs, { Dayjs } from 'dayjs';
import ExpressSettingModal from './expressSettingModal.vue';
const useForm = Form.useForm;
const props = defineProps<{
visible: boolean;
data?: ShopOrder | null;
}>();
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
(e: 'done'): void;
}>();
// 表单数据
const form = reactive({
deliveryType: 0, // 0快递配送 1无需发货 2商家送货
deliveryMethod: 'manual', // manual手动填写 print电子面单打印
expressId: undefined as number | undefined,
expressName: '',
trackingNumber: '',
partialDelivery: false,
deliveryNote: '',
sendName: '',
sendPhone: '',
sendAddress: '',
deliveryTime: dayjs() as Dayjs
});
// 表单验证规则
const rules = {
deliveryType: [{ required: true, message: '请选择配送方式' }],
deliveryMethod: [
{
validator: (_: any, value: any) => {
if (form.deliveryType === 0 && !value) {
return Promise.reject('请选择发货类型');
}
return Promise.resolve();
}
}
],
expressId: [
{
validator: (_: any, value: any) => {
if (form.deliveryType === 0 && !value) {
return Promise.reject('请选择快递公司');
}
return Promise.resolve();
}
}
],
// trackingNumber: [
// {
// required: true,
// message: '请输入快递单号',
// validator: (_: any, value: any) => {
// if (form.deliveryType === 0 && !value) {
// return Promise.reject('请输入快递单号');
// }
// return Promise.resolve();
// }
// }
// ],
deliveryTime: [{ required: true, message: '请选择发货时间' }],
sendName: [
{
validator: (_: any, value: any) => {
if (form.deliveryType !== 1 && !value) {
return Promise.reject('请输入发货人');
}
return Promise.resolve();
}
}
],
sendPhone: [
{
validator: (_: any, value: any) => {
if (form.deliveryType !== 1 && !value) {
return Promise.reject('请输入发货人联系方式');
}
return Promise.resolve();
}
}
],
sendAddress: [
{
validator: (_: any, value: any) => {
if (form.deliveryType !== 1 && !value) {
return Promise.reject('请输入发货地址');
}
return Promise.resolve();
}
}
]
};
const formRef = ref();
const { resetFields, validate } = useForm(form, rules);
// 状态
const loading = ref(false);
const expressList = ref<ShopExpress[]>([]);
const expressModalVisible = ref(false);
// 加载快递公司列表
const loadExpressList = async () => {
try {
const data = await listShopExpress({});
expressList.value = data || [];
} catch (error) {
console.error('加载快递公司失败:', error);
}
};
// 快递公司筛选
const filterExpressOption = (input: string, option: any) => {
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
// 快递公司选择变化
const onExpressChange = (value: number) => {
const express = expressList.value.find((item) => item.expressId === value);
if (express) {
form.expressName = express.expressName || '';
}
};
// 打开快递公司设置
const openExpressModal = () => {
expressModalVisible.value = true;
};
// 根据配送方式重置不需要的字段
watch(
() => form.deliveryType,
(type) => {
if (type !== 0) {
form.expressId = undefined;
form.expressName = '';
form.deliveryMethod = 'manual';
}
if (type === 1) {
form.sendName = '';
form.sendPhone = '';
form.sendAddress = '';
}
}
);
// 更新弹窗显示状态
const updateVisible = (visible: boolean) => {
emit('update:visible', visible);
};
// 取消
const handleCancel = () => {
updateVisible(false);
};
// 提交发货
const handleSubmit = async () => {
try {
await validate();
loading.value = true;
const deliveryTime = toDateString(
form.deliveryTime.toDate(),
'yyyy-MM-dd HH:mm:ss'
);
const updateData = {
...props.data,
deliveryStatus: 20, // 已发货
deliveryType: form.deliveryType,
deliveryTime: deliveryTime,
deliveryNote: form.deliveryNote
};
// 如果是快递配送,添加快递信息
if (form.deliveryType === 0) {
updateData.expressId = form.expressId;
updateData.sendName = form.sendName;
updateData.sendPhone = form.sendPhone;
updateData.sendAddress = form.sendAddress;
// updateData.expressName = form.expressName;
// updateData.trackingNumber = form.trackingNumber;
} else if (form.deliveryType === 2) {
// 商家送货需要记录发货人信息,但不需要快递公司
updateData.sendName = form.sendName;
updateData.sendPhone = form.sendPhone;
updateData.sendAddress = form.sendAddress;
updateData.expressId = undefined;
} else {
// 无需发货,清理快递/发货信息
updateData.expressId = undefined;
updateData.sendName = undefined;
updateData.sendPhone = undefined;
updateData.sendAddress = undefined;
}
// 分单发货
if (form.partialDelivery) {
updateData.deliveryStatus = 30;
}
await updateShopOrder(updateData);
message.success('发货成功');
emit('done');
updateVisible(false);
} catch (error: any) {
console.log(error);
message.error(error.message || '发货失败');
} finally {
loading.value = false;
}
};
// 监听弹窗显示状态
watch(
() => props.visible,
(visible) => {
if (visible) {
// 重置表单
form.deliveryType = 0;
form.deliveryMethod = 'manual';
form.expressId = undefined;
form.expressName = '';
form.trackingNumber = '';
form.partialDelivery = false;
form.deliveryNote = '';
form.sendName = '';
form.sendPhone = '';
form.sendAddress = '';
form.deliveryTime = dayjs();
// 加载快递公司列表
loadExpressList();
} else {
resetFields();
}
}
);
</script>
<style scoped>
.ant-radio-wrapper {
margin-right: 16px;
}
</style>

View File

@@ -0,0 +1,372 @@
<!-- 快递公司设置弹窗 -->
<template>
<a-modal
:visible="visible"
title="设置物流公司"
width="900px"
:confirm-loading="loading"
@update:visible="updateVisible"
@ok="handleSubmit"
@cancel="handleCancel"
>
<div class="express-setting">
<!-- 搜索栏 -->
<div class="search-bar" style="margin-bottom: 16px">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索快递公司名称"
style="width: 300px"
@search="handleSearch"
/>
<a-button type="primary" style="margin-left: 8px" @click="openAddModal">
添加快递公司
</a-button>
</div>
<!-- 快递公司列表 -->
<a-table
:data-source="filteredExpressList"
:columns="columns"
:pagination="false"
:scroll="{ y: 400 }"
row-key="expressId"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-switch
v-model:checked="record.enabled"
checked-children="启用"
un-checked-children="禁用"
@change="handleStatusChange(record)"
/>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="editExpress(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此快递公司吗?"
@confirm="deleteExpress(record)"
>
<a class="text-red-500">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 添加/编辑快递公司弹窗 -->
<a-modal
v-model:visible="addModalVisible"
:title="editingExpress ? '编辑快递公司' : '添加快递公司'"
width="500px"
:confirm-loading="addLoading"
@ok="handleAddSubmit"
@cancel="handleAddCancel"
>
<a-form
ref="addFormRef"
:model="addForm"
:rules="addRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="快递公司名称" name="expressName">
<a-input
v-model:value="addForm.expressName"
placeholder="请输入快递公司名称"
:maxlength="50"
/>
</a-form-item>
<a-form-item label="微信编码" name="wxCode">
<a-input
v-model:value="addForm.wxCode"
placeholder="请输入微信快递编码(可选)"
:maxlength="20"
/>
</a-form-item>
<a-form-item label="快递100编码" name="kuaidi100Code">
<a-input
v-model:value="addForm.kuaidi100Code"
placeholder="请输入快递100编码可选"
:maxlength="20"
/>
</a-form-item>
<a-form-item label="快递鸟编码" name="kdniaoCode">
<a-input
v-model:value="addForm.kdniaoCode"
placeholder="请输入快递鸟编码(可选)"
:maxlength="20"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
v-model:value="addForm.sortNumber"
placeholder="数字越小越靠前"
:min="0"
:max="9999"
style="width: 100%"
/>
</a-form-item>
</a-form>
</a-modal>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { ShopExpress } from '@/api/shop/shopExpress/model';
import {
listShopExpress,
addShopExpress,
updateShopExpress,
removeShopExpress
} from '@/api/shop/shopExpress';
const useForm = Form.useForm;
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
(e: 'done'): void;
}>();
// 状态
const loading = ref(false);
const searchKeyword = ref('');
const expressList = ref<ShopExpress[]>([]);
const addModalVisible = ref(false);
const addLoading = ref(false);
const editingExpress = ref<ShopExpress | null>(null);
// 添加表单
const addForm = reactive({
expressName: '',
wxCode: '',
kuaidi100Code: '',
kdniaoCode: '',
sortNumber: 0
});
const addRules = {
expressName: [
{ required: true, message: '请输入快递公司名称' },
{ min: 2, max: 50, message: '快递公司名称长度为2-50个字符' }
]
};
const addFormRef = ref();
const { resetFields: resetAddFields, validate: validateAdd } = useForm(
addForm,
addRules
);
// 表格列定义
const columns = [
{
title: '快递公司名称',
dataIndex: 'expressName',
key: 'expressName',
width: 150
},
{
title: '微信编码',
dataIndex: 'wxCode',
key: 'wxCode',
width: 100
},
{
title: '快递100编码',
dataIndex: 'kuaidi100Code',
key: 'kuaidi100Code',
width: 120
},
{
title: '快递鸟编码',
dataIndex: 'kdniaoCode',
key: 'kdniaoCode',
width: 120
},
{
title: '排序号',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 80,
align: 'center'
},
{
title: '状态',
key: 'status',
width: 100,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
align: 'center'
}
];
// 过滤后的快递公司列表
const filteredExpressList = computed(() => {
if (!searchKeyword.value) {
return expressList.value;
}
return expressList.value.filter((item) =>
item.expressName
?.toLowerCase()
.includes(searchKeyword.value.toLowerCase())
);
});
// 加载快递公司列表
const loadExpressList = async () => {
try {
loading.value = true;
const data = await listShopExpress({});
expressList.value = (data || []).map((item) => ({
...item,
enabled: item.deleted === 0 // 假设deleted=0表示启用
}));
} catch (error) {
console.error('加载快递公司失败:', error);
message.error('加载快递公司失败');
} finally {
loading.value = false;
}
};
// 搜索
const handleSearch = () => {
// 搜索逻辑已在computed中处理
};
// 状态变化
const handleStatusChange = async (
record: ShopExpress & { enabled: boolean }
) => {
try {
await updateShopExpress({
...record,
deleted: record.enabled ? 0 : 1
});
message.success(record.enabled ? '已启用' : '已禁用');
} catch (error: any) {
message.error(error.message || '操作失败');
// 恢复状态
record.enabled = !record.enabled;
}
};
// 打开添加弹窗
const openAddModal = () => {
editingExpress.value = null;
resetAddFields();
addModalVisible.value = true;
};
// 编辑快递公司
const editExpress = (record: ShopExpress) => {
editingExpress.value = record;
addForm.expressName = record.expressName || '';
addForm.wxCode = record.wxCode || '';
addForm.kuaidi100Code = record.kuaidi100Code || '';
addForm.kdniaoCode = record.kdniaoCode || '';
addForm.sortNumber = record.sortNumber || 0;
addModalVisible.value = true;
};
// 删除快递公司
const deleteExpress = async (record: ShopExpress) => {
try {
if (record.expressId) {
await removeShopExpress(record.expressId);
message.success('删除成功');
loadExpressList();
}
} catch (error: any) {
message.error(error.message || '删除失败');
}
};
// 添加/编辑提交
const handleAddSubmit = async () => {
try {
await validateAdd();
addLoading.value = true;
if (editingExpress.value) {
// 编辑
await updateShopExpress({
...editingExpress.value,
...addForm
});
message.success('编辑成功');
} else {
// 添加
await addShopExpress(addForm);
message.success('添加成功');
}
addModalVisible.value = false;
loadExpressList();
} catch (error: any) {
message.error(error.message || '操作失败');
} finally {
addLoading.value = false;
}
};
// 取消添加/编辑
const handleAddCancel = () => {
addModalVisible.value = false;
resetAddFields();
};
// 更新弹窗显示状态
const updateVisible = (visible: boolean) => {
emit('update:visible', visible);
};
// 取消
const handleCancel = () => {
updateVisible(false);
};
// 确定
const handleSubmit = () => {
emit('done');
updateVisible(false);
};
// 监听弹窗显示状态
watch(
() => props.visible,
(visible) => {
if (visible) {
loadExpressList();
}
}
);
</script>
<style scoped>
.express-setting {
min-height: 400px;
}
.search-bar {
display: flex;
align-items: center;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,265 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button-->
<!-- danger-->
<!-- type="primary"-->
<!-- class="ele-btn-icon"-->
<!-- :disabled="selection?.length === 0"-->
<!-- @click="removeBatch"-->
<!-- >-->
<!-- <template #icon>-->
<!-- <DeleteOutlined/>-->
<!-- </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"
placeholder="订单类型"
@change="search"
>
<a-select-option value="">全部</a-select-option>
<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-select
v-model:value="where.payStatus"
style="width: 150px"
placeholder="付款状态"
@change="search"
>
<a-select-option value="">全部</a-select-option>
<a-select-option :value="1">已付款</a-select-option>
<a-select-option :value="0">未付款</a-select-option>
</a-select>
<!-- <a-select-->
<!-- v-model:value="where.orderStatus"-->
<!-- style="width: 150px"-->
<!-- placeholder="订单状态"-->
<!-- @change="search"-->
<!-- >-->
<!-- <a-select-option value="">全部</a-select-option>-->
<!-- <a-select-option :value="1">已完成</a-select-option>-->
<!-- <a-select-option :value="0">未完成</a-select-option>-->
<!-- <a-select-option :value="2">未使用</a-select-option>-->
<!-- <a-select-option :value="3">已取消</a-select-option>-->
<!-- <a-select-option :value="4">退款中</a-select-option>-->
<!-- <a-select-option :value="5">退款被拒</a-select-option>-->
<!-- <a-select-option :value="6">退款成功</a-select-option>-->
<!-- </a-select>-->
<a-select
:options="getPayType()"
v-model:value="where.payType"
style="width: 150px"
placeholder="付款方式"
@change="search"
/>
<a-range-picker
v-model:value="dateRange"
@change="search"
value-format="YYYY-MM-DD"
/>
<a-input-search
allow-clear
: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>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { utils, writeFile } from 'xlsx';
import { message } from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import { ShopOrder, ShopOrderParam } from '@/api/shop/shopOrder/model';
import { listShopOrder } from '@/api/shop/shopOrder';
import { getPayType } from '@/utils/shop';
const props = withDefaults(
defineProps<{
// 选中的订单
selection?: ShopOrder[];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopOrderParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const { where, resetFields } = useSearch<ShopOrderParam>({
keywords: '',
orderId: undefined,
orderNo: undefined,
createTimeStart: undefined,
createTimeEnd: undefined,
userId: undefined,
payUserId: undefined,
nickname: undefined,
phone: undefined,
payStatus: undefined,
orderStatus: undefined,
payType: undefined
});
const reload = () => {
emit('search', {
...where,
keywords: type.value == '' ? where.keywords : undefined
});
};
// 批量删除
// 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 ?? [];
xlsFileName.value = `${d1}${d2}`;
where.createTimeStart = d1 ? d1 + ' 00:00:00' : undefined;
where.createTimeEnd = d2 ? d2 + ' 23:59:59' : undefined;
emit('search', {
...where
});
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
const dateRange = ref<[string, string]>(['', '']);
// 变量
const loading = ref(false);
const orders = ref<ShopOrder[]>([]);
const xlsFileName = ref<string>();
const type = ref('');
// 导出
const handleExport = async () => {
loading.value = true;
const array: (string | number)[][] = [
[
'订单编号',
'订单标题',
'买家姓名',
'手机号码',
'实付金额(元)',
'支付方式',
'付款时间',
'下单时间'
]
];
await listShopOrder(where)
.then((list) => {
orders.value = list;
list?.forEach((d: ShopOrder) => {
array.push([
`${d.orderNo}`,
`${d.comments}`,
`${d.realName}`,
`${d.phone}`,
`${d.payPrice}`,
`${getPayType(d.payType)}`,
`${d.payTime || ''}`,
`${d.createTime}`
]);
});
const sheetName = `订单数据`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 },
{ wch: 40 },
{ wch: 20 },
{ wch: 20 },
{ wch: 60 },
{ wch: 15 },
{ wch: 10 },
{ wch: 10 },
{ wch: 20 },
{ wch: 10 },
{ wch: 20 }
];
message.loading('正在导出...');
setTimeout(() => {
writeFile(
workbook,
`${
where.createTimeEnd ? xlsFileName.value + '_' : ''
}${sheetName}.xlsx`
);
loading.value = false;
}, 1000);
})
.catch((msg) => {
message.error(msg);
loading.value = false;
})
.finally(() => {});
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,726 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card style="margin-bottom: 20px">
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
<a-tab-pane key="all" tab="全部" />
<a-tab-pane key="unpaid" tab="待付款" />
<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-tabs>
<ele-pro-table
ref="tableRef"
row-key="orderId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:toolbar="false"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar> </template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div @click="onSearch(record)" class="cursor-pointer">{{
record.name || '匿名'
}}</div>
</template>
<template v-if="column.key === 'orderGoods'">
<template v-for="(item, index) in record.orderGoods" :key="index">
<div class="item py-1">
<a-space :id="`g-${index}`">
<a-avatar :src="item.image" shape="square" />
<span>{{ item.goodsName }}</span>
</a-space>
</div>
</template>
</template>
<template v-if="column.key === 'phone'">
<div v-if="record.mobile" class="text-gray-400">{{
record.mobile
}}</div>
<div v-else class="text-gray-600">{{ record.phone }}</div>
</template>
<template v-if="column.key === 'payType'">
<template v-for="item in getPayType()">
<template v-if="record.payStatus == 1">
<span v-if="item.value == record.payType">{{
item.label
}}</span>
</template>
<template v-else>
<span></span>
</template>
</template>
</template>
<template v-if="column.key === 'payStatus'">
<a-tag
v-if="record.payStatus"
color="green"
@click.stop="updatePayStatus(record)"
class="cursor-pointer"
>已付款
</a-tag>
<a-tag
v-if="!record.payStatus"
@click.stop="updatePayStatus(record)"
class="cursor-pointer"
>未付款
</a-tag>
<a-tag v-if="record.payStatus == 3">未付款,占场中</a-tag>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'sex'">
<a-tag v-if="record.sex === 1"></a-tag>
<a-tag v-if="record.sex === 2"></a-tag>
</template>
<template v-if="column.key === 'deliveryStatus'">
<a-tag v-if="record.deliveryStatus == 10">未发货</a-tag>
<a-tag v-if="record.deliveryStatus == 20" color="green"
>已发货</a-tag
>
<a-tag v-if="record.deliveryStatus == 30" color="blue"
>部分发货</a-tag
>
</template>
<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">已关闭</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
>
<a-tag v-if="record.orderStatus === 7" color="pink"
>客户端申请退款</a-tag
>
</template>
<template v-if="column.key === 'isInvoice'">
<a-tag v-if="record.isInvoice == 0">未开具</a-tag>
<a-tag v-if="record.isInvoice == 1" color="green">已开具</a-tag>
<a-tag v-if="record.isInvoice == 2" color="blue">不能开具</a-tag>
</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 @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a>
<!-- 未付款状态的操作 -->
<template v-if="!record.payStatus && record.orderStatus === 0">
<a-divider type="vertical" />
<a @click.stop="handleEditOrder(record)">
<EditOutlined /> 修改
</a>
<a-divider type="vertical" />
<a @click.stop="handleCancelOrder(record)">
<span class="ele-text-warning"> <CloseOutlined /> 关闭 </span>
</a>
</template>
<!-- 已付款未发货状态的操作 -->
<template
v-if="
record.payStatus &&
record.deliveryStatus === 10 &&
!isCancelledStatus(record.orderStatus)
"
>
<a-divider type="vertical" />
<a @click.stop="handleDelivery(record)" class="ele-text-primary">
<SendOutlined /> 发货
</a>
<a-divider type="vertical" />
<a @click.stop="handleApplyRefund(record)">
<UndoOutlined /> 退款
</a>
</template>
<!-- 已发货未完成状态的操作 -->
<template
v-if="
record.payStatus &&
record.deliveryStatus === 20 &&
record.orderStatus === 0
"
>
<a-divider type="vertical" />
<a
@click.stop="handleConfirmReceive(record)"
class="ele-text-primary"
>
<CheckOutlined /> 确认收货
</a>
<a-divider type="vertical" />
<a @click.stop="handleApplyRefund(record)">
<UndoOutlined /> 退款
</a>
</template>
<!-- 退款相关状态的操作 -->
<template v-if="isRefundStatus(record.orderStatus)">
<template
v-if="record.orderStatus === 4 || record.orderStatus === 7"
>
<a-divider type="vertical" />
<a
@click.stop="handleApproveRefund(record)"
class="ele-text-success"
>
<CheckCircleOutlined /> 同意退款
</a>
<a-divider type="vertical" />
<a
@click.stop="handleRejectRefund(record)"
class="ele-text-danger"
>
<CloseCircleOutlined /> 拒绝退款
</a>
</template>
<template v-if="record.orderStatus === 5">
<a-divider type="vertical" />
<a @click.stop="handleRetryRefund(record)">
<RedoOutlined /> 重新处理
</a>
</template>
</template>
<!-- 已完成状态的操作 -->
<template v-if="record.orderStatus === 1">
<a-divider type="vertical" />
<a @click.stop="handleApplyRefund(record)">
<UndoOutlined /> 申请退款
</a>
</template>
<!-- 删除操作 - 已完成、已关闭、退款成功的订单可以删除 -->
<template v-if="canDeleteOrder(record)">
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此订单吗?删除后无法恢复。"
@confirm="remove(record)"
>
<a class="ele-text-danger" @click.stop>
<DeleteOutlined /> 删除
</a>
</a-popconfirm>
</template>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<OrderInfo v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 发货弹窗 -->
<DeliveryModal
v-model:visible="showDelivery"
:data="current"
@done="reload"
/>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import {
ExclamationCircleOutlined,
EyeOutlined,
EditOutlined,
CloseOutlined,
SendOutlined,
UndoOutlined,
CheckOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
RedoOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import Search from './components/search.vue';
import { getPageTitle } from '@/utils/common';
import { toDateString } from 'ele-admin-pro';
import OrderInfo from './components/orderInfo.vue';
import DeliveryModal from './components/deliveryModal.vue';
import { ShopOrder, ShopOrderParam } from '@/api/shop/shopOrder/model';
import {
pageShopOrder,
repairOrder,
removeShopOrder,
removeBatchShopOrder,
updateShopOrder
} from '@/api/shop/shopOrder';
import { updateUser } from '@/api/system/user';
import { getPayType } from '@/utils/shop';
import { message, Modal } from 'ant-design-vue';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopOrder[]>([]);
// 当前编辑数据
const current = ref<ShopOrder | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 是否显示发货弹窗
const showDelivery = ref(false);
// 加载状态
const loading = ref(true);
// 激活的标签
const activeKey = ref<string>('all');
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 0;
return pageShopOrder({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '订单编号',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center',
width: 200
},
{
title: '商品信息',
dataIndex: 'orderGoods',
key: 'orderGoods',
width: 360
},
{
title: '实付金额',
dataIndex: 'payPrice',
key: 'payPrice',
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '支付方式',
dataIndex: 'payType',
key: 'payType',
align: 'center'
},
{
title: '支付状态',
dataIndex: 'payStatus',
key: 'payStatus',
align: 'center'
},
{
title: '发货状态',
dataIndex: 'deliveryStatus',
key: 'deliveryStatus',
align: 'center'
},
{
title: '开票状态',
dataIndex: 'isInvoice',
key: 'isInvoice',
align: 'center'
},
{
title: '订单状态',
dataIndex: 'orderStatus',
key: 'orderStatus',
align: 'center'
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// align: 'center',
// },
// {
// title: '支付时间',
// dataIndex: 'payTime',
// key: 'payTime',
// align: 'center',
// width: 180,
// sorter: true,
// ellipsis: true
// },
{
title: '下单时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 180,
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 280,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopOrderParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
const onTabs = () => {
// 使用statusFilter进行筛选这是后端专门为订单状态筛选设计的字段
const filterParams: Record<string, any> = {};
// 根据后端 statusFilter 的值对应:
// undefined全部0待付款1待发货2待核销3待收货4待评价5已完成6已退款7已删除
switch (activeKey.value) {
case 'all':
// 全部订单不传statusFilter参数
// filterParams.statusFilter = undefined; // 不设置该字段
break;
case 'unpaid':
// 待付款pay_status = false
filterParams.statusFilter = 0;
break;
case 'undelivered':
// 待发货pay_status = true AND delivery_status = 10
filterParams.statusFilter = 1;
break;
case 'unverified':
// 待核销pay_status = true AND delivery_status = 10 (与待发货相同)
filterParams.statusFilter = 2;
break;
case 'unreceived':
// 待收货pay_status = true AND delivery_status = 20
filterParams.statusFilter = 3;
break;
case 'unevaluated':
// 待评价order_status = 1 (与已完成相同)
filterParams.statusFilter = 4;
break;
case 'completed':
// 已完成order_status = 1
filterParams.statusFilter = 5;
break;
case 'cancelled':
// 已关闭order_status = 2
filterParams.statusFilter = 8;
break;
case 'refunded':
// 退款/售后order_status = 6
filterParams.statusFilter = 6;
break;
case 'deleted':
// 已删除deleted = 1
filterParams.statusFilter = 7;
break;
}
reload(filterParams);
};
const onSearch = (item: ShopOrder) => {
reload({ userId: item.userId });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopOrder) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/**
* 修复订单支付状态
*/
const updatePayStatus = (record: ShopOrder) => {
// 修复订单数据
repairOrder({
...record
}).then(() => {
if (!record.realName) {
// 更新用户真实姓名
updateUser({
userId: record.userId,
realName: record.realName
}).then(() => {});
}
reload();
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 辅助判断函数 */
// 判断是否为关闭状态
const isCancelledStatus = (orderStatus?: number) => {
return [2, 3].includes(orderStatus || 0);
};
// 判断是否为退款相关状态
const isRefundStatus = (orderStatus?: number) => {
return [4, 5, 6, 7].includes(orderStatus || 0);
};
// 判断是否可以删除订单
const canDeleteOrder = (order: ShopOrder) => {
// 已完成、已关闭、退款成功的订单可以删除 (原来是[1, 2, 6],后面改成只有关闭的订单能删除)
return [2].includes(order.orderStatus || 0);
};
/* 订单操作方法 */
// 修改订单
const handleEditOrder = (record: ShopOrder) => {
message.info('订单修改功能开发中...');
// TODO: 实现订单修改功能
};
// 关闭订单
const handleCancelOrder = (record: ShopOrder) => {
Modal.confirm({
title: '确认关闭订单',
content: '确定要关闭此订单吗?关闭后无法恢复。',
onOk: async () => {
try {
await updateShopOrder({
...record,
orderStatus: 2 // 已关闭
});
message.success('订单已关闭');
reload();
} catch (error: any) {
message.error(error.message || '关闭订单失败');
}
}
});
};
// 发货处理
const handleDelivery = (record: ShopOrder) => {
current.value = record;
showDelivery.value = true;
};
// 确认收货
const handleConfirmReceive = (record: ShopOrder) => {
Modal.confirm({
title: '确认收货',
content: '确定要将此订单标记为已收货并完成吗?',
onOk: async () => {
try {
await updateShopOrder({
...record,
deliveryStatus: 30, // 已收货
orderStatus: 1 // 已完成
});
message.success('确认收货成功');
reload();
} catch (error: any) {
message.error(error.message || '确认收货失败');
}
}
});
};
// 同意退款
const handleApproveRefund = (record: ShopOrder) => {
Modal.confirm({
title: '同意退款',
content: '确定要同意此订单的退款申请吗?',
onOk: async () => {
try {
const now = new Date();
const refundTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss');
await updateShopOrder({
...record,
orderStatus: 6, // 退款成功
refundTime: refundTime
});
message.success('退款处理成功');
reload();
} catch (error: any) {
message.error(error.message || '退款处理失败');
}
}
});
};
// 拒绝退款
const handleRejectRefund = (record: ShopOrder) => {
Modal.confirm({
title: '拒绝退款',
content: '确定要拒绝此订单的退款申请吗?',
onOk: async () => {
try {
await updateShopOrder({
...record,
orderStatus: 5 // 退款被拒绝
});
message.success('已拒绝退款申请');
reload();
} catch (error: any) {
message.error(error.message || '操作失败');
}
}
});
};
// 重新处理退款
const handleRetryRefund = (record: ShopOrder) => {
Modal.confirm({
title: '重新处理退款',
content: '确定要重新处理此订单的退款吗?',
onOk: async () => {
try {
await updateShopOrder({
...record,
orderStatus: 4 // 退款申请中
});
message.success('已重新提交退款申请');
reload();
} catch (error: any) {
message.error(error.message || '操作失败');
}
}
});
};
// 申请退款
const handleApplyRefund = (record: ShopOrder) => {
Modal.confirm({
title: '申请退款',
content: '确定要为此订单申请退款吗?',
onOk: async () => {
try {
const now = new Date();
const refundApplyTime = toDateString(now, 'yyyy-MM-dd HH:mm:ss');
await updateShopOrder({
...record,
orderStatus: 4, // 退款申请中
refundApplyTime: refundApplyTime
});
message.success('退款申请已提交');
reload();
} catch (error: any) {
message.error(error.message || '申请退款失败');
}
}
});
};
/* 删除单个订单 */
const remove = (row: ShopOrder) => {
removeShopOrder(row.orderId)
.then(() => {
message.success('删除成功');
reload();
})
.catch((e) => {
message.error(e.message);
});
};
/* 批量删除订单 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const ids = selection.value.map((d) => d.orderId);
removeBatchShopOrder(ids)
.then(() => {
message.success('删除成功');
reload();
})
.catch((e) => {
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: ShopOrder) => {
return {
// 行点击事件
onClick: () => {
openEdit(record);
},
// 行双击事件
onDblclick: () => {
// openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'BszxOrder',
components: MenuIcons
};
</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,359 @@
<!-- 编辑弹窗 -->
<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="关联订单表id" name="orderId">
<a-input
allow-clear
placeholder="请输入关联订单表id"
v-model:value="form.orderId"
/>
</a-form-item>
<a-form-item label="订单标识" name="orderCode">
<a-input
allow-clear
placeholder="请输入订单标识"
v-model:value="form.orderCode"
/>
</a-form-item>
<a-form-item label="关联商户ID" name="merchantId">
<a-input
allow-clear
placeholder="请输入关联商户ID"
v-model:value="form.merchantId"
/>
</a-form-item>
<a-form-item label="商户名称" name="merchantName">
<a-input
allow-clear
placeholder="请输入商户名称"
v-model:value="form.merchantName"
/>
</a-form-item>
<a-form-item label="商品封面图" name="image">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</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="goodsName">
<a-input
allow-clear
placeholder="请输入商品名称"
v-model:value="form.goodsName"
/>
</a-form-item>
<a-form-item label="商品规格" name="spec">
<a-input
allow-clear
placeholder="请输入商品规格"
v-model:value="form.spec"
/>
</a-form-item>
<a-form-item label="" name="skuId">
<a-input allow-clear placeholder="请输入" v-model:value="form.skuId" />
</a-form-item>
<a-form-item label="单价" name="price">
<a-input
allow-clear
placeholder="请输入单价"
v-model:value="form.price"
/>
</a-form-item>
<a-form-item label="购买数量" name="totalNum">
<a-input
allow-clear
placeholder="请输入购买数量"
v-model:value="form.totalNum"
/>
</a-form-item>
<a-form-item
label="0 未付款 1已付款2无需付款或占用状态"
name="payStatus"
>
<a-input
allow-clear
placeholder="请输入0 未付款 1已付款2无需付款或占用状态"
v-model:value="form.payStatus"
/>
</a-form-item>
<a-form-item
label="0未使用1已完成2已取消3取消中4退款申请中5退款被拒绝6退款成功7客户端申请退款"
name="orderStatus"
>
<a-input
allow-clear
placeholder="请输入0未使用1已完成2已取消3取消中4退款申请中5退款被拒绝6退款成功7客户端申请退款"
v-model:value="form.orderStatus"
/>
</a-form-item>
<a-form-item label="是否免费0收费、1免费" name="isFree">
<a-input
allow-clear
placeholder="请输入是否免费0收费、1免费"
v-model:value="form.isFree"
/>
</a-form-item>
<a-form-item label="系统版本 0当前版本 其他版本" name="version">
<a-input
allow-clear
placeholder="请输入系统版本 0当前版本 其他版本"
v-model:value="form.version"
/>
</a-form-item>
<a-form-item label="预约时间段" name="timePeriod">
<a-input
allow-clear
placeholder="请输入预约时间段"
v-model:value="form.timePeriod"
/>
</a-form-item>
<a-form-item label="预定日期" name="dateTime">
<a-input
allow-clear
placeholder="请输入预定日期"
v-model:value="form.dateTime"
/>
</a-form-item>
<a-form-item label="开场时间" name="startTime">
<a-input
allow-clear
placeholder="请输入开场时间"
v-model:value="form.startTime"
/>
</a-form-item>
<a-form-item label="结束时间" name="endTime">
<a-input
allow-clear
placeholder="请输入结束时间"
v-model:value="form.endTime"
/>
</a-form-item>
<a-form-item label="毫秒时间戳" name="timeFlag">
<a-input
allow-clear
placeholder="请输入毫秒时间戳"
v-model:value="form.timeFlag"
/>
</a-form-item>
<a-form-item label="过期时间" name="expirationTime">
<a-input
allow-clear
placeholder="请输入过期时间"
v-model:value="form.expirationTime"
/>
</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="用户id" name="userId">
<a-input
allow-clear
placeholder="请输入用户id"
v-model:value="form.userId"
/>
</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 {
addShopOrderGoods,
updateShopOrderGoods
} from '@/api/shop/shopOrderGoods';
import { ShopOrderGoods } from '@/api/shop/shopOrderGoods/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?: ShopOrderGoods | 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<ShopOrderGoods>({
id: undefined,
orderId: undefined,
orderCode: undefined,
merchantId: undefined,
merchantName: undefined,
image: undefined,
goodsId: undefined,
goodsName: undefined,
spec: undefined,
skuId: undefined,
price: undefined,
totalNum: undefined,
payStatus: undefined,
orderStatus: undefined,
isFree: undefined,
version: undefined,
timePeriod: undefined,
dateTime: undefined,
startTime: undefined,
endTime: undefined,
timeFlag: undefined,
expirationTime: undefined,
comments: undefined,
userId: undefined,
tenantId: undefined,
updateTime: undefined,
createTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopOrderGoodsName: [
{
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
? updateShopOrderGoods
: addShopOrderGoods;
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,373 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopOrderGoodsId"
: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>
<!-- 编辑弹窗 -->
<ShopOrderGoodsEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
</div>
</div>
</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 ShopOrderGoodsEdit from './components/shopOrderGoodsEdit.vue';
import {
pageShopOrderGoods,
removeShopOrderGoods,
removeBatchShopOrderGoods
} from '@/api/shop/shopOrderGoods';
import type {
ShopOrderGoods,
ShopOrderGoodsParam
} from '@/api/shop/shopOrderGoods/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopOrderGoods[]>([]);
// 当前编辑数据
const current = ref<ShopOrderGoods | 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 pageShopOrderGoods({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '自增ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '关联订单表id',
dataIndex: 'orderId',
key: 'orderId',
align: 'center'
},
{
title: '订单标识',
dataIndex: 'orderCode',
key: 'orderCode',
align: 'center'
},
{
title: '关联商户ID',
dataIndex: 'merchantId',
key: 'merchantId',
align: 'center'
},
{
title: '商户名称',
dataIndex: 'merchantName',
key: 'merchantName',
align: 'center'
},
{
title: '商品封面图',
dataIndex: 'image',
key: 'image',
align: 'center'
},
{
title: '关联商品id',
dataIndex: 'goodsId',
key: 'goodsId',
align: 'center'
},
{
title: '商品名称',
dataIndex: 'goodsName',
key: 'goodsName',
align: 'center'
},
{
title: '商品规格',
dataIndex: 'spec',
key: 'spec',
align: 'center'
},
{
title: '',
dataIndex: 'skuId',
key: 'skuId',
align: 'center'
},
{
title: '单价',
dataIndex: 'price',
key: 'price',
align: 'center'
},
{
title: '购买数量',
dataIndex: 'totalNum',
key: 'totalNum',
align: 'center'
},
{
title: '0 未付款 1已付款2无需付款或占用状态',
dataIndex: 'payStatus',
key: 'payStatus',
align: 'center'
},
{
title:
'0未使用1已完成2已取消3取消中4退款申请中5退款被拒绝6退款成功7客户端申请退款',
dataIndex: 'orderStatus',
key: 'orderStatus',
align: 'center'
},
{
title: '是否免费0收费、1免费',
dataIndex: 'isFree',
key: 'isFree',
align: 'center'
},
{
title: '系统版本 0当前版本 其他版本',
dataIndex: 'version',
key: 'version',
align: 'center'
},
{
title: '预约时间段',
dataIndex: 'timePeriod',
key: 'timePeriod',
align: 'center'
},
{
title: '预定日期',
dataIndex: 'dateTime',
key: 'dateTime',
align: 'center'
},
{
title: '开场时间',
dataIndex: 'startTime',
key: 'startTime',
align: 'center'
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
align: 'center'
},
{
title: '毫秒时间戳',
dataIndex: 'timeFlag',
key: 'timeFlag',
align: 'center'
},
{
title: '过期时间',
dataIndex: 'expirationTime',
key: 'expirationTime',
align: 'center'
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center'
},
{
title: '用户id',
dataIndex: 'userId',
key: 'userId',
align: 'center'
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopOrderGoodsParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopOrderGoods) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopOrderGoods) => {
const hide = message.loading('请求中..', 0);
removeShopOrderGoods(row.shopOrderGoodsId)
.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);
removeBatchShopOrderGoods(
selection.value.map((d) => d.shopOrderGoodsId)
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopOrderGoods) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopOrderGoods'
};
</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,223 @@
<!-- 编辑弹窗 -->
<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="specName">
<a-input
allow-clear
placeholder="请输入规格名称"
v-model:value="form.specName"
/>
</a-form-item>
<a-form-item label="规格值" name="specValue">
<a-input
allow-clear
placeholder="请输入规格值"
v-model:value="form.specValue"
/>
</a-form-item>
<a-form-item label="商户ID" name="merchantId">
<a-input
allow-clear
placeholder="请输入商户ID"
v-model:value="form.merchantId"
/>
</a-form-item>
<a-form-item label="创建用户" name="userId">
<a-input
allow-clear
placeholder="请输入创建用户"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="更新者" name="updater">
<a-input
allow-clear
placeholder="请输入更新者"
v-model:value="form.updater"
/>
</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="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="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 { addShopSpec, updateShopSpec } from '@/api/shop/shopSpec';
import { ShopSpec } from '@/api/shop/shopSpec/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?: ShopSpec | 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<ShopSpec>({
specId: undefined,
specName: undefined,
specValue: undefined,
merchantId: undefined,
userId: undefined,
updater: undefined,
comments: undefined,
status: undefined,
sortNumber: undefined,
tenantId: undefined,
createTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopSpecName: [
{
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 ? updateShopSpec : addShopSpec;
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,267 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopSpecId"
: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>
<!-- 编辑弹窗 -->
<ShopSpecEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</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 ShopSpecEdit from './components/shopSpecEdit.vue';
import {
pageShopSpec,
removeShopSpec,
removeBatchShopSpec
} from '@/api/shop/shopSpec';
import type { ShopSpec, ShopSpecParam } from '@/api/shop/shopSpec/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopSpec[]>([]);
// 当前编辑数据
const current = ref<ShopSpec | 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 pageShopSpec({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '规格ID',
dataIndex: 'specId',
key: 'specId',
align: 'center',
width: 90
},
{
title: '规格名称',
dataIndex: 'specName',
key: 'specName',
align: 'center'
},
{
title: '规格值',
dataIndex: 'specValue',
key: 'specValue',
align: 'center'
},
{
title: '商户ID',
dataIndex: 'merchantId',
key: 'merchantId',
align: 'center'
},
{
title: '创建用户',
dataIndex: 'userId',
key: 'userId',
align: 'center'
},
{
title: '更新者',
dataIndex: 'updater',
key: 'updater',
align: 'center'
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center'
},
{
title: '状态, 0正常, 1待修,2异常已修3异常未修',
dataIndex: 'status',
key: 'status',
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')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopSpecParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopSpec) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopSpec) => {
const hide = message.loading('请求中..', 0);
removeShopSpec(row.shopSpecId)
.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);
removeBatchShopSpec(selection.value.map((d) => d.shopSpecId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopSpec) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopSpec'
};
</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,197 @@
<!-- 编辑弹窗 -->
<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="规格组ID" name="specId">
<a-input
allow-clear
placeholder="请输入规格组ID"
v-model:value="form.specId"
/>
</a-form-item>
<a-form-item label="规格值" name="specValue">
<a-input
allow-clear
placeholder="请输入规格值"
v-model:value="form.specValue"
/>
</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>
</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 {
addShopSpecValue,
updateShopSpecValue
} from '@/api/shop/shopSpecValue';
import { ShopSpecValue } from '@/api/shop/shopSpecValue/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?: ShopSpecValue | 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<ShopSpecValue>({
specValueId: undefined,
specId: undefined,
specValue: undefined,
comments: undefined,
tenantId: undefined,
createTime: undefined,
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopSpecValueName: [
{
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
? updateShopSpecValue
: addShopSpecValue;
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,250 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="shopSpecValueId"
: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>
<!-- 编辑弹窗 -->
<ShopSpecValueEdit
v-model:visible="showEdit"
:data="current"
@done="reload"
/>
</div>
</div>
</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 ShopSpecValueEdit from './components/shopSpecValueEdit.vue';
import {
pageShopSpecValue,
removeShopSpecValue,
removeBatchShopSpecValue
} from '@/api/shop/shopSpecValue';
import type {
ShopSpecValue,
ShopSpecValueParam
} from '@/api/shop/shopSpecValue/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopSpecValue[]>([]);
// 当前编辑数据
const current = ref<ShopSpecValue | 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 pageShopSpecValue({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '规格值ID',
dataIndex: 'specValueId',
key: 'specValueId',
align: 'center',
width: 90
},
{
title: '规格组ID',
dataIndex: 'specId',
key: 'specId',
align: 'center'
},
{
title: '规格值',
dataIndex: 'specValue',
key: 'specValue',
align: 'center'
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
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')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopSpecValueParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopSpecValue) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopSpecValue) => {
const hide = message.loading('请求中..', 0);
removeShopSpecValue(row.shopSpecValueId)
.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);
removeBatchShopSpecValue(selection.value.map((d) => d.shopSpecValueId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopSpecValue) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopSpecValue'
};
</script>
<style lang="less" scoped></style>

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

@@ -0,0 +1,88 @@
<!-- 用户导入弹窗 -->
<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>
<a
href="https://server.websoft.top/api/system/user/import/template"
download="用户导入模板.xlsx"
>
下载导入模板
</a>
</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 { importUsers } from '@/api/system/user';
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;
importUsers(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

@@ -0,0 +1,606 @@
<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

@@ -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,327 @@
<!-- 编辑弹窗 -->
<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="优惠券模板ID" name="couponId">
<a-input
allow-clear
placeholder="请输入优惠券模板ID"
v-model:value="form.couponId"
/>
</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="优惠券名称" name="name">
<a-input
allow-clear
placeholder="请输入优惠券名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="优惠券描述" name="description">
<a-input
allow-clear
placeholder="请输入优惠券描述"
v-model:value="form.description"
/>
</a-form-item>
<a-form-item label="优惠券类型(10满减券 20折扣券 30免费劵)" name="type">
<a-input
allow-clear
placeholder="请输入优惠券类型(10满减券 20折扣券 30免费劵)"
v-model:value="form.type"
/>
</a-form-item>
<a-form-item label="满减券-减免金额" name="reducePrice">
<a-input
allow-clear
placeholder="请输入满减券-减免金额"
v-model:value="form.reducePrice"
/>
</a-form-item>
<a-form-item label="折扣券-折扣率(0-100)" name="discount">
<a-input
allow-clear
placeholder="请输入折扣券-折扣率(0-100)"
v-model:value="form.discount"
/>
</a-form-item>
<a-form-item label="最低消费金额" name="minPrice">
<a-input
allow-clear
placeholder="请输入最低消费金额"
v-model:value="form.minPrice"
/>
</a-form-item>
<a-form-item
label="适用范围(10全部商品 20指定商品 30指定分类)"
name="applyRange"
>
<a-input
allow-clear
placeholder="请输入适用范围(10全部商品 20指定商品 30指定分类)"
v-model:value="form.applyRange"
/>
</a-form-item>
<a-form-item label="适用范围配置(json格式)" name="applyRangeConfig">
<a-input
allow-clear
placeholder="请输入适用范围配置(json格式)"
v-model:value="form.applyRangeConfig"
/>
</a-form-item>
<a-form-item label="有效期开始时间" name="startTime">
<a-input
allow-clear
placeholder="请输入有效期开始时间"
v-model:value="form.startTime"
/>
</a-form-item>
<a-form-item label="有效期结束时间" name="endTime">
<a-input
allow-clear
placeholder="请输入有效期结束时间"
v-model:value="form.endTime"
/>
</a-form-item>
<a-form-item label="使用状态(0未使用 1已使用 2已过期)" 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="useTime">
<a-input
allow-clear
placeholder="请输入使用时间"
v-model:value="form.useTime"
/>
</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="orderNo">
<a-input
allow-clear
placeholder="请输入使用订单号"
v-model:value="form.orderNo"
/>
</a-form-item>
<a-form-item
label="获取方式(10主动领取 20系统发放 30活动赠送)"
name="obtainType"
>
<a-input
allow-clear
placeholder="请输入获取方式(10主动领取 20系统发放 30活动赠送)"
v-model:value="form.obtainType"
/>
</a-form-item>
<a-form-item label="获取来源描述" name="obtainSource">
<a-input
allow-clear
placeholder="请输入获取来源描述"
v-model:value="form.obtainSource"
/>
</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 {
addShopUserCoupon,
updateShopUserCoupon
} from '@/api/shop/shopUserCoupon';
import { ShopUserCoupon } from '@/api/shop/shopUserCoupon/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?: ShopUserCoupon | 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<ShopUserCoupon>({
id: undefined,
couponId: undefined,
userId: undefined,
name: undefined,
description: undefined,
type: undefined,
reducePrice: undefined,
discount: undefined,
minPrice: undefined,
applyRange: undefined,
applyRangeConfig: undefined,
startTime: undefined,
endTime: undefined,
status: undefined,
useTime: undefined,
orderId: undefined,
orderNo: undefined,
obtainType: undefined,
obtainSource: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopUserCouponName: [
{
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
? updateShopUserCoupon
: addShopUserCoupon;
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,347 @@
<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="shopUserCouponId"
: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>
<!-- 编辑弹窗 -->
<ShopUserCouponEdit
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 ShopUserCouponEdit from './components/shopUserCouponEdit.vue';
import {
pageShopUserCoupon,
removeShopUserCoupon,
removeBatchShopUserCoupon
} from '@/api/shop/shopUserCoupon';
import type {
ShopUserCoupon,
ShopUserCouponParam
} from '@/api/shop/shopUserCoupon/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopUserCoupon[]>([]);
// 当前编辑数据
const current = ref<ShopUserCoupon | 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 pageShopUserCoupon({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'id',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '优惠券模板ID',
dataIndex: 'couponId',
key: 'couponId',
align: 'center'
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center'
},
{
title: '优惠券名称',
dataIndex: 'name',
key: 'name',
align: 'center'
},
{
title: '优惠券描述',
dataIndex: 'description',
key: 'description',
align: 'center'
},
{
title: '优惠券类型(10满减券 20折扣券 30免费劵)',
dataIndex: 'type',
key: 'type',
align: 'center'
},
{
title: '满减券-减免金额',
dataIndex: 'reducePrice',
key: 'reducePrice',
align: 'center'
},
{
title: '折扣券-折扣率(0-100)',
dataIndex: 'discount',
key: 'discount',
align: 'center'
},
{
title: '最低消费金额',
dataIndex: 'minPrice',
key: 'minPrice',
align: 'center'
},
{
title: '适用范围(10全部商品 20指定商品 30指定分类)',
dataIndex: 'applyRange',
key: 'applyRange',
align: 'center'
},
{
title: '适用范围配置(json格式)',
dataIndex: 'applyRangeConfig',
key: 'applyRangeConfig',
align: 'center'
},
{
title: '有效期开始时间',
dataIndex: 'startTime',
key: 'startTime',
align: 'center'
},
{
title: '有效期结束时间',
dataIndex: 'endTime',
key: 'endTime',
align: 'center'
},
{
title: '使用状态(0未使用 1已使用 2已过期)',
dataIndex: 'status',
key: 'status',
align: 'center'
},
{
title: '使用时间',
dataIndex: 'useTime',
key: 'useTime',
align: 'center'
},
{
title: '使用订单ID',
dataIndex: 'orderId',
key: 'orderId',
align: 'center'
},
{
title: '使用订单号',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center'
},
{
title: '获取方式(10主动领取 20系统发放 30活动赠送)',
dataIndex: 'obtainType',
key: 'obtainType',
align: 'center'
},
{
title: '获取来源描述',
dataIndex: 'obtainSource',
key: 'obtainSource',
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: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopUserCouponParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopUserCoupon) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopUserCoupon) => {
const hide = message.loading('请求中..', 0);
removeShopUserCoupon(row.shopUserCouponId)
.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);
removeBatchShopUserCoupon(
selection.value.map((d) => d.shopUserCouponId)
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopUserCoupon) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopUserCoupon'
};
</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,211 @@
<!-- 编辑弹窗 -->
<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="推荐人ID" name="dealerId">
<a-input
allow-clear
placeholder="请输入推荐人ID"
v-model:value="form.dealerId"
/>
</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="推荐关系层级(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="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</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 {
addShopUserReferee,
updateShopUserReferee
} from '@/api/shop/shopUserReferee';
import { ShopUserReferee } from '@/api/shop/shopUserReferee/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?: ShopUserReferee | 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<ShopUserReferee>({
id: undefined,
dealerId: undefined,
userId: undefined,
level: undefined,
comments: undefined,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopUserRefereeName: [
{
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
? updateShopUserReferee
: addShopUserReferee;
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,263 @@
<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="shopUserRefereeId"
: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>
<!-- 编辑弹窗 -->
<ShopUserRefereeEdit
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 ShopUserRefereeEdit from './components/shopUserRefereeEdit.vue';
import {
pageShopUserReferee,
removeShopUserReferee,
removeBatchShopUserReferee
} from '@/api/shop/shopUserReferee';
import type {
ShopUserReferee,
ShopUserRefereeParam
} from '@/api/shop/shopUserReferee/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopUserReferee[]>([]);
// 当前编辑数据
const current = ref<ShopUserReferee | 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 pageShopUserReferee({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '主键ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90
},
{
title: '推荐人ID',
dataIndex: 'dealerId',
key: 'dealerId',
align: 'center'
},
{
title: '用户id(被推荐人)',
dataIndex: 'userId',
key: 'userId',
align: 'center'
},
{
title: '推荐关系层级(1,2,3)',
dataIndex: 'level',
key: 'level',
align: 'center'
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
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: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopUserRefereeParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopUserReferee) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopUserReferee) => {
const hide = message.loading('请求中..', 0);
removeShopUserReferee(row.shopUserRefereeId)
.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);
removeBatchShopUserReferee(
selection.value.map((d) => d.shopUserRefereeId)
)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopUserReferee) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopUserReferee'
};
</script>
<style lang="less" scoped></style>