This commit is contained in:
2026-05-31 12:09:14 +08:00
commit e4f9eedb80
1121 changed files with 209380 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,483 @@
<template>
<div class="shop-dashboard">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="welcome-left">
<h2 class="welcome-title">🏸 {{ siteStore.appName || '桂礼序' }}</h2>
<p class="welcome-sub">欢迎回来管理员今日数据已更新</p>
</div>
<div class="welcome-right">
<a-space>
<a-tag color="green" style="font-size:13px;padding:4px 12px">{{ siteStore.statusText || '正常' }}</a-tag>
<a-button size="small" @click="refreshStatistics" :loading="loading">
<template #icon><ReloadOutlined /></template>
刷新数据
</a-button>
</a-space>
</div>
</div>
<!-- 核心数据统计 -->
<a-row :gutter="[16, 16]">
<a-col :xs="12" :sm="12" :md="6" v-for="stat in coreStats" :key="stat.label">
<div class="stat-block" :class="stat.color" @click="navigateTo(stat.to)" :style="{ cursor: stat.to ? 'pointer' : 'default' }">
<div class="stat-block-header">
<span class="stat-block-icon">{{ stat.icon }}</span>
<span class="stat-block-label">{{ stat.label }}</span>
</div>
<div class="stat-block-value">
<template v-if="loading">
<a-skeleton-input :active="true" size="small" style="width:60px" />
</template>
<template v-else>{{ stat.value }}</template>
</div>
<div class="stat-block-desc">{{ stat.desc }}</div>
</div>
</a-col>
</a-row>
<!-- 待办事项 + 快捷操作 -->
<a-row :gutter="[16, 16]">
<!-- 待处理事项 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🔔 待处理事项</span>
</div>
<div class="todo-list">
<div
v-for="todo in todoItems"
:key="todo.label"
class="todo-item"
:class="{ 'todo-item-urgent': todo.urgent }"
@click="navigateTo(todo.to)"
>
<div class="todo-dot" :class="todo.dotColor"></div>
<div class="todo-content">
<span class="todo-label">{{ todo.label }}</span>
<a-tag :color="todo.tagColor" style="margin-left:8px">
<template v-if="loading">...</template>
<template v-else>{{ todo.value }}</template>
</a-tag>
</div>
<RightOutlined class="todo-arrow" />
</div>
<div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">
🎉 暂无待处理事项一切正常
</div>
</div>
</div>
</a-col>
<!-- 快速入口 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快速入口</span>
</div>
<div class="quick-grid">
<div
v-for="item in quickLinks"
:key="item.to"
class="quick-card"
@click="navigateTo(item.to)"
>
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
<div class="quick-label">{{ item.label }}</div>
</div>
</div>
</div>
</a-col>
</a-row>
<!-- 今日数据趋势 -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">📊 今日数据概况</span>
</div>
<div class="today-stats">
<div class="today-stat">
<div class="today-stat-icon">💰</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.salesAmount || '0.00' }}</div>
<div class="today-stat-label">今日销售额</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">📦</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.orderCount || 0 }}</div>
<div class="today-stat-label">今日订单数</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">👥</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.newUsers || 0 }}</div>
<div class="today-stat-label">新增用户</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">🎁</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.couponUsed || 0 }}</div>
<div class="today-stat-label">使用优惠券</div>
</div>
</div>
</div>
</div>
<!-- 系统基本信息 -->
<a-row :gutter="[16, 16]" style="margin-top: 16px">
<a-col :span="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📋 基本信息</span>
</div>
<div style="padding: 16px 18px;">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="系统名称">
{{ siteStore.appName }}
</a-descriptions-item>
<a-descriptions-item label="版本号">
{{ systemInfo.version }}
</a-descriptions-item>
<a-descriptions-item label="运行状态">
{{ siteStore.statusText || '正常' }}
</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="系统运行">
{{ runDays }}
</a-descriptions-item>
</a-descriptions>
</div>
</div>
</a-col>
<!-- 快捷操作 -->
<a-col :span="12">
<div class="panel" style="min-height: 353px">
<div class="panel-header">
<span class="panel-title"> 快捷操作</span>
</div>
<div style="padding: 16px 18px;">
<a-space direction="vertical" style="width: 100%">
<a-button
type="primary"
block
@click="navigateTo('/website/field')"
:loading="loading"
>
<UngroupOutlined />
参数配置
</a-button>
<a-button block @click="navigateTo('/shopOrder')">
<CalendarOutlined />
订单管理
</a-button>
<a-button block @click="navigateTo('/system/user')">
<UserOutlined />
用户管理
</a-button>
<a-button block @click="navigateTo('/website/index')">
<ShopOutlined />
站点管理
</a-button>
<a-button block @click="navigateTo('/system/login-record')">
<FileTextOutlined />
登录日志
</a-button>
<a-button block @click="handleClearCache">
<ClearOutlined />
清除缓存
</a-button>
</a-space>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import {
ReloadOutlined,
RightOutlined,
UngroupOutlined,
CalendarOutlined,
UserOutlined,
ShopOutlined,
FileTextOutlined,
ClearOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue/es';
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();
const router = useRouter();
// 从 store 中获取响应式数据
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
const { loading: statisticsLoading } = storeToRefs(statisticsStore);
// 系统信息
const systemInfo = reactive({
name: '小程序商城',
version: '2.0.0',
environment: '生产环境',
database: 'MySQL 8.0',
server: 'Linux CentOS 7.9',
});
// 计算属性
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 coreStats = reactive([
{ icon: '🏸', label: '用户总数', value: 0, desc: '注册用户', color: 'blue', to: '/system/user' },
{ icon: '📦', label: '订单总数', value: 0, desc: '全部订单', color: 'orange', to: '/shopOrder' },
{ icon: '💰', label: '总营业额', value: '0.00', desc: '元', color: 'purple', to: '/shopOrder' },
{ icon: '⏱️', label: '运行天数', value: 0, desc: '系统运行', color: 'green', to: '' },
]);
// 待处理事项
const todoItems = reactive([
{ label: '待发货订单', value: 0, to: '/shopOrder', tagColor: 'blue', dotColor: 'dot-blue', urgent: false },
{ label: '退款申请', value: 0, to: '/shopOrder', tagColor: 'orange', dotColor: 'dot-orange', urgent: false },
{ label: '优惠券使用', value: 0, to: '/shopGoodsCoupon', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
]);
// 今日统计
const todayStats = reactive({
salesAmount: '0.00',
orderCount: 0,
newUsers: 0,
couponUsed: 0,
});
// 快速入口
const quickLinks = [
{ to: '/website/field', icon: '⚙️', label: '参数配置', bg: '#eff6ff' },
{ to: '/shopOrder', icon: '📦', label: '订单管理', bg: '#f0fdf4' },
{ to: '/system/user', icon: '👥', label: '用户管理', bg: '#fff7ed' },
{ to: '/website/index', icon: '🏪', label: '站点管理', bg: '#faf5ff' },
{ to: '/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
{ to: '/cmsArticle', icon: '📝', label: '文章管理', bg: '#fefce8' },
];
// 导航跳转
const navigateTo = (path: string) => {
if (path) {
router.push(path);
}
};
// 清除缓存
const handleClearCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
(msg) => {
if (msg) {
message.success(msg);
}
}
);
};
// 刷新统计数据
const refreshStatistics = async () => {
try {
await statisticsStore.fetchStatistics();
message.success('统计数据刷新成功');
} catch (error) {
console.error('刷新统计数据失败:', error);
message.error('刷新统计数据失败');
}
};
// 加载数据
const loadData = async () => {
try {
await Promise.all([
siteStore.fetchSiteInfo(),
statisticsStore.fetchStatistics()
]);
// 更新核心统计数据
coreStats[0].value = userCount.value || 0;
coreStats[1].value = orderCount.value || 0;
coreStats[2].value = totalSales.value || '0.00';
coreStats[3].value = runDays.value || 0;
// 模拟今日数据(实际应从后端统计接口获取)
todayStats.salesAmount = totalSales.value || '0.00';
todayStats.orderCount = orderCount.value || 0;
todayStats.newUsers = Math.floor((userCount.value || 0) * 0.1);
todayStats.couponUsed = 0;
// 待处理事项(可以从 store 中获取真实数据)
// todoItems[0].value = ... // 待发货订单数
// todoItems[1].value = ... // 退款申请数
} catch (error) {
console.error('加载数据失败:', error);
}
};
onMounted(async () => {
await loadData();
// 开始自动刷新统计数据每5分钟
statisticsStore.startAutoRefresh();
});
onUnmounted(() => {
// 组件卸载时停止自动刷新
statisticsStore.stopAutoRefresh();
});
</script>
<style scoped>
.shop-dashboard {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px 24px;
}
/* 欢迎横幅 */
.welcome-banner {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #00704A 0%, #1a3931 100%);
border-radius: 14px;
padding: 24px 28px;
color: #fff;
flex-wrap: wrap;
gap: 12px;
}
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
/* 核心统计块 */
.stat-block {
padding: 18px 20px;
border-radius: 12px;
border: 2px solid transparent;
transition: all 0.2s;
}
.stat-block:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.stat-block.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-block.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-block.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-block.purple { background: #faf5ff; border-color: #e9d5ff; }
.stat-block-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.stat-block-icon { font-size: 18px; }
.stat-block-label { font-size: 13px; color: rgba(0,0,0,0.55); }
.stat-block-value { font-size: 32px; font-weight: 800; color: rgba(0,0,0,0.85); line-height: 1.1; margin-bottom: 4px; }
.stat-block-desc { font-size: 12px; color: rgba(0,0,0,0.4); }
/* Panel */
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
.panel-header { padding: 14px 18px; border-bottom: 1px solid #f5f5f5; }
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
/* 待办 */
.todo-list { padding: 8px 0; }
.todo-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
}
.todo-item:hover { background: #f9fafb; }
.todo-item-urgent .todo-label { font-weight: 600; }
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-orange { background: #f97316; }
.dot-blue { background: #3b82f6; }
.dot-cyan { background: #06b6d4; }
.dot-purple { background: #a855f7; }
.todo-content { flex: 1; display: flex; align-items: center; }
.todo-label { font-size: 14px; color: rgba(0,0,0,0.75); }
.todo-arrow { font-size: 11px; color: rgba(0,0,0,0.3); }
.todo-empty { text-align: center; padding: 20px 0; color: rgba(0,0,0,0.4); font-size: 14px; }
/* 快速入口九宫格 */
.quick-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: #f5f5f5;
}
.quick-card {
display: flex; flex-direction: column; align-items: center;
gap: 8px; padding: 18px 12px; background: #fff;
cursor: pointer; transition: background 0.15s;
}
.quick-card:hover { background: #f9fafb; }
.quick-icon {
width: 44px; height: 44px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 22px;
}
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
/* 今日数据 */
.today-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 20px;
}
.today-stat {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #fafafa;
border-radius: 8px;
}
.today-stat-icon {
font-size: 28px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.today-stat-value {
font-size: 20px;
font-weight: 700;
color: #1a3931;
}
.today-stat-label {
font-size: 12px;
color: #666;
margin-top: 2px;
}
@media (max-width: 768px) {
.today-stats {
grid-template-columns: repeat(2, 1fr);
}
}
</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,261 @@
<!-- 编辑弹窗 -->
<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="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="type">
<a-radio-group v-model:value="form.type">
<a-radio :value="1">满减</a-radio>
<a-radio :value="2">折扣</a-radio>
<a-radio :value="3">秒杀</a-radio>
<a-radio :value="4">拼团</a-radio>
</a-radio-group>
</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="description">
<a-textarea
allow-clear
:rows="3"
placeholder="请输入活动描述"
v-model:value="form.description"
/>
</a-form-item>
<a-form-item label="活动开始时间" name="startTime">
<a-date-picker
show-time
format="YYYY-MM-DD HH:mm:ss"
class="ele-fluid"
placeholder="请选择活动开始时间"
v-model:value="startTimeValue"
@change="onStartTimeChange"
/>
</a-form-item>
<a-form-item label="活动结束时间" name="endTime">
<a-date-picker
show-time
format="YYYY-MM-DD HH:mm:ss"
class="ele-fluid"
placeholder="请选择活动结束时间"
v-model:value="endTimeValue"
@change="onEndTimeChange"
/>
</a-form-item>
<a-form-item label="活动规则" name="rules">
<a-textarea
allow-clear
:rows="4"
placeholder="请输入活动规则(JSON格式)"
v-model:value="form.rules"
/>
</a-form-item>
<a-form-item label="商户ID" name="merchantId">
<a-input-number
:min="0"
class="ele-fluid"
placeholder="请输入商户ID"
v-model:value="form.merchantId"
/>
</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>
</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 { addShopActivity, updateShopActivity } from '@/api/shop/shopActivity';
import { ShopActivity } from '@/api/shop/shopActivity/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import dayjs from 'dayjs';
import {
PlusOutlined
} from '@ant-design/icons-vue';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
visible: boolean;
data?: ShopActivity | 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 startTimeValue = ref<dayjs.Dayjs | null>(null);
const endTimeValue = ref<dayjs.Dayjs | null>(null);
const form = reactive<ShopActivity>({
id: undefined,
name: undefined,
description: undefined,
image: undefined,
banner: undefined,
type: 1,
startTime: undefined,
endTime: undefined,
status: 0,
rules: undefined,
merchantId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请填写活动名称',
trigger: 'blur'
}
]
});
// 日期选择处理
const onStartTimeChange = (_: any, dateString: string) => {
form.startTime = dateString;
};
const onEndTimeChange = (_: any, dateString: string) => {
form.endTime = dateString;
};
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;
// 将字段列表转为JSON字符串
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateShopActivity : addShopActivity;
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'
});
}
// 解析日期
if (props.data.startTime) {
startTimeValue.value = dayjs(props.data.startTime);
}
if (props.data.endTime) {
endTimeValue.value = dayjs(props.data.endTime);
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
<style scoped></style>

View File

@@ -0,0 +1,272 @@
<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="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 === 'type'">
<a-tag v-if="record.type === 1" color="blue">满减</a-tag>
<a-tag v-if="record.type === 2" color="green">折扣</a-tag>
<a-tag v-if="record.type === 3" color="orange">秒杀</a-tag>
<a-tag v-if="record.type === 4" color="purple">拼团</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>
<!-- 编辑弹窗 -->
<ShopActivityEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } 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 ShopActivityEdit from './components/shopActivityEdit.vue';
import { pageShopActivity, removeShopActivity, removeBatchShopActivity } from '@/api/shop/shopActivity';
import type { ShopActivity, ShopActivityParam } from '@/api/shop/shopActivity/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopActivity[]>([]);
// 当前编辑数据
const current = ref<ShopActivity | 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 pageShopActivity({
...where,
...orders,
page,
limit
});
};
// 完整的列配置
const allColumns = ref<ColumnItem[]>([
{
title: '活动ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '活动名称',
dataIndex: 'name',
key: 'name',
ellipsis: true
},
{
title: '活动类型',
dataIndex: 'type',
key: 'type',
width: 120
},
{
title: '活动封面图',
dataIndex: 'image',
key: 'image',
width: 80,
ellipsis: true
},
{
title: '活动开始时间',
dataIndex: 'startTime',
key: 'startTime',
width: 160
},
{
title: '活动结束时间',
dataIndex: 'endTime',
key: 'endTime',
width: 160
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm')
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列
const defaultVisibleColumns = [
'name',
'type',
'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: ShopActivityParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopActivity) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopActivity) => {
const hide = message.loading('请求中..', 0);
removeShopActivity(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);
removeBatchShopActivity(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: ShopActivity) => {
return {
onClick: () => {
// console.log(record);
},
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopActivity'
};
</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,241 @@
<!-- 编辑弹窗 -->
<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="activityId">
<a-input
allow-clear
placeholder="请输入活动ID"
v-model:value="form.activityId"
/>
</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="formData">
<a-textarea
allow-clear
:rows="4"
placeholder="请输入报名表单填写数据(JSON格式)"
v-model:value="form.formData"
/>
</a-form-item>
<a-form-item label="支付金额" name="payAmount">
<a-input-number
:min="0"
:precision="2"
class="ele-fluid"
placeholder="请输入支付金额"
v-model:value="form.payAmount"
/>
</a-form-item>
<a-form-item label="支付状态" name="payStatus">
<a-radio-group v-model:value="form.payStatus">
<a-radio :value="0">未支付</a-radio>
<a-radio :value="1">已支付</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="支付时间" name="payTime">
<a-date-picker
show-time
format="YYYY-MM-DD HH:mm:ss"
class="ele-fluid"
placeholder="请选择支付时间"
v-model:value="payTimeValue"
@change="onPayTimeChange"
/>
</a-form-item>
<a-form-item label="第三方交易号" name="transactionId">
<a-input
allow-clear
placeholder="请输入第三方交易号"
v-model:value="form.transactionId"
/>
</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>
</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 { addShopActivitySignup, updateShopActivitySignup } from '@/api/shop/shopActivitySignup';
import { ShopActivitySignup } from '@/api/shop/shopActivitySignup/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import dayjs from 'dayjs';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopActivitySignup | 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 payTimeValue = ref<dayjs.Dayjs | null>(null);
// 用户信息
const form = reactive<ShopActivitySignup>({
signupId: undefined,
activityId: undefined,
userId: undefined,
formData: undefined,
payAmount: undefined,
payStatus: 0,
payTime: undefined,
transactionId: undefined,
status: 0,
deleted: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopActivitySignupId: undefined,
shopActivitySignupName: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopActivitySignupName: [
{
required: true,
type: 'string',
message: '请填写活动报名表名称',
trigger: 'blur'
}
]
});
const onPayTimeChange = (_: any, dateString: string) => {
form.payTime = dateString;
};
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 ? updateShopActivitySignup : addShopActivitySignup;
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'
})
}
// 解析支付时间
if (props.data.payTime) {
payTimeValue.value = dayjs(props.data.payTime);
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,291 @@
<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="shopActivitySignupId"
: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 === 'payStatus'">
<a-tag v-if="record.payStatus === 0" color="orange">未支付</a-tag>
<a-tag v-if="record.payStatus === 1" color="green">已支付</a-tag>
</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="red">已取消</a-tag>
</template>
<template v-if="column.key === 'formData'">
<a-tooltip>
<template #title>
<pre style="max-width: 300px; white-space: pre-wrap">{{ formatFormData(record.formData) }}</pre>
</template>
<a class="ele-text-primary" style="cursor: pointer">查看表单数据</a>
</a-tooltip>
</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>
<!-- 编辑弹窗 -->
<ShopActivitySignupEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } 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 ShopActivitySignupEdit from './components/shopActivitySignupEdit.vue';
import { pageShopActivitySignup, removeShopActivitySignup, removeBatchShopActivitySignup } from '@/api/shop/shopActivitySignup';
import type { ShopActivitySignup, ShopActivitySignupParam } from '@/api/shop/shopActivitySignup/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopActivitySignup[]>([]);
// 当前编辑数据
const current = ref<ShopActivitySignup | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 格式化表单数据
const formatFormData = (formData?: string) => {
if (!formData) return '无数据';
try {
const obj = JSON.parse(formData);
return JSON.stringify(obj, null, 2);
} catch {
return formData;
}
};
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopActivitySignup({
...where,
...orders,
page,
limit
});
};
// 完整的列配置
const allColumns = ref<ColumnItem[]>([
{
title: '报名ID',
dataIndex: 'signupId',
key: 'signupId',
width: 90,
},
{
title: '活动ID',
dataIndex: 'activityId',
key: 'activityId',
width: 100
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 100
},
{
title: '表单数据',
dataIndex: 'formData',
key: 'formData',
ellipsis: true
},
{
title: '支付金额',
dataIndex: 'payAmount',
key: 'payAmount',
width: 100
},
{
title: '支付状态',
dataIndex: 'payStatus',
key: 'payStatus',
width: 100
},
{
title: '支付时间',
dataIndex: 'payTime',
key: 'payTime',
width: 160
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm')
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列
const defaultVisibleColumns = [
'signupId',
'activityId',
'payStatus',
'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: ShopActivitySignupParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopActivitySignup) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopActivitySignup) => {
const hide = message.loading('请求中..', 0);
removeShopActivitySignup(row.shopActivitySignupId)
.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);
removeBatchShopActivitySignup(selection.value.map((d) => d.shopActivitySignupId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ShopActivitySignup) => {
return {
onClick: () => {
// console.log(record);
},
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopActivitySignup'
};
</script>
<style lang="less" scoped></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,603 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<a-space>
<a-button @click="reload()">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
</a-space>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<!-- 统计卡片 -->
<a-row :gutter="16" class="stats-row">
<a-col :span="6">
<a-statistic title="今日预约" :value="stats.todayCount" :value-style="{ color: '#0e932e' }">
<template #suffix>
<span class="stat-suffix"></span>
</template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="本周预约" :value="stats.weekCount" :value-style="{ color: '#1890ff' }">
<template #suffix>
<span class="stat-suffix"></span>
</template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="本月预约" :value="stats.monthCount" :value-style="{ color: '#722ed1' }">
<template #suffix>
<span class="stat-suffix"></span>
</template>
</a-statistic>
</a-col>
<a-col :span="6">
<a-statistic title="待确认" :value="stats.pendingCount" :value-style="{ color: '#faad14' }">
<template #suffix>
<span class="stat-suffix"></span>
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0' }" class="mt-4">
<!-- 搜索区域 -->
<div class="search-container" style="padding: 16px">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="门店筛选">
<a-select
v-model:value="searchForm.storeId"
placeholder="请选择门店"
allow-clear
style="width: 180px"
:loading="storeLoading"
@change="handleStoreChange"
>
<a-select-option :value="undefined">全部门店</a-select-option>
<a-select-option v-for="store in storeList" :key="store.id" :value="store.id">
{{ store.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="预约状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 120px"
>
<a-select-option value="">全部状态</a-select-option>
<a-select-option value="pending">待确认</a-select-option>
<a-select-option value="confirmed">已确认</a-select-option>
<a-select-option value="in_progress">进行中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
<a-select-option value="rescheduled">已改签</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="关键词">
<a-input
v-model:value="searchForm.keywords"
placeholder="编号/姓名/电话"
allow-clear
style="width: 150px"
/>
</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>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
:scroll="{ x: 1200 }"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'bookingNo'">
<span class="booking-no">{{ record.bookingNo || record.id }}</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'price'">
<span class="price">¥{{ record.price || 0 }}</span>
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDateTime(record.createTime) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewDetail(record)">
详情
</a-button>
<a-dropdown v-if="record.status === 'pending'">
<a-button type="link" size="small">
操作 <DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="confirm" @click="updateStatus(record.id, 'confirmed')">
确认预约
</a-menu-item>
<a-menu-item key="cancel" @click="updateStatus(record.id, 'cancelled')">
取消预约
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-dropdown v-if="record.status === 'confirmed'">
<a-button type="link" size="small">
操作 <DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="progress" @click="updateStatus(record.id, 'in_progress')">
开始服务
</a-menu-item>
<a-menu-item key="complete" @click="updateStatus(record.id, 'completed')">
完成服务
</a-menu-item>
<a-menu-item key="cancel" @click="updateStatus(record.id, 'cancelled')">
取消预约
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情弹窗 -->
<a-modal
v-model:open="detailVisible"
title="预约详情"
:footer="null"
width="600px"
>
<a-descriptions :column="2" bordered v-if="currentRecord">
<a-descriptions-item label="预约编号">
{{ currentRecord.bookingNo || currentRecord.id }}
</a-descriptions-item>
<a-descriptions-item label="预约状态">
<a-tag :color="getStatusColor(currentRecord.status)">
{{ getStatusText(currentRecord.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="门店名称" :span="2">
{{ currentRecord.storeName }}
</a-descriptions-item>
<a-descriptions-item label="预约日期">
{{ currentRecord.bookingDate }}
</a-descriptions-item>
<a-descriptions-item label="预约时段">
{{ currentRecord.bookingTime }}
</a-descriptions-item>
<a-descriptions-item label="服务类型" :span="2">
{{ currentRecord.serviceName }}
</a-descriptions-item>
<a-descriptions-item label="预约费用">
¥{{ currentRecord.price || 0 }}
</a-descriptions-item>
<a-descriptions-item label="联系人">
{{ currentRecord.contactName }}
</a-descriptions-item>
<a-descriptions-item label="联系电话" :span="2">
{{ currentRecord.contactPhone }}
</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">
{{ currentRecord.remark || '无' }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ formatDateTime(currentRecord.createTime) }}
</a-descriptions-item>
<a-descriptions-item label="用户ID">
{{ currentRecord.userId }}
</a-descriptions-item>
</a-descriptions>
</a-modal>
</a-page-header>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
ReloadOutlined,
SearchOutlined,
ClearOutlined,
DownOutlined
} from '@ant-design/icons-vue'
import request from '@/utils/request'
import type { ApiResult, PageResult } from '@/api'
import dayjs from 'dayjs'
interface ShopBooking {
id: number
bookingNo: string
storeId: number
storeName: string
serviceName: string
bookingDate: string
bookingTime: string
status: string
price: number
remark: string
contactName: string
contactPhone: string
userId: number
createTime: string
}
interface ShopStore {
id: number
name: string
}
export default defineComponent({
name: 'ShopBookingList',
components: {
ReloadOutlined,
SearchOutlined,
ClearOutlined,
DownOutlined
},
setup() {
const loading = ref(false)
const storeLoading = ref(false)
const dataSource = ref<ShopBooking[]>([])
const storeList = ref<ShopStore[]>([])
const detailVisible = ref(false)
const currentRecord = ref<ShopBooking | null>(null)
const stats = reactive({
todayCount: 0,
weekCount: 0,
monthCount: 0,
pendingCount: 0,
totalCount: 0
})
const searchForm = reactive({
storeId: undefined as number | undefined,
status: '' as string,
keywords: ''
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total}`
})
const columns = [
{
title: '预约编号',
key: 'bookingNo',
width: 150,
fixed: 'left'
},
{
title: '门店名称',
dataIndex: 'storeName',
key: 'storeName',
width: 150,
ellipsis: true
},
{
title: '预约日期',
dataIndex: 'bookingDate',
key: 'bookingDate',
width: 120
},
{
title: '预约时段',
dataIndex: 'bookingTime',
key: 'bookingTime',
width: 120
},
{
title: '服务类型',
dataIndex: 'serviceName',
key: 'serviceName',
width: 100
},
{
title: '联系人',
dataIndex: 'contactName',
key: 'contactName',
width: 80
},
{
title: '联系电话',
dataIndex: 'contactPhone',
key: 'contactPhone',
width: 120
},
{
title: '状态',
key: 'status',
width: 90
},
{
title: '费用',
key: 'price',
width: 80
},
{
title: '创建时间',
key: 'createTime',
width: 160
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right'
}
]
// 获取页面标题
const getPageTitle = () => {
return '穿线预约订单'
}
// 状态颜色映射
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
pending: 'orange',
confirmed: 'blue',
in_progress: 'cyan',
completed: 'green',
cancelled: 'red',
rescheduled: 'purple'
}
return colorMap[status] || 'default'
}
// 状态文本映射
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
pending: '待确认',
confirmed: '已确认',
in_progress: '进行中',
completed: '已完成',
cancelled: '已取消',
rescheduled: '已改签'
}
return textMap[status] || status
}
// 格式化日期时间
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
return dayjs(dateTime).format('YYYY-MM-DD HH:mm')
}
// 获取统计数据
const fetchStats = async () => {
try {
const params: any = {}
if (searchForm.storeId) {
params.storeId = searchForm.storeId
}
const res = await request.get<ApiResult<any>>('/shop/shop-booking/admin/stats', { params })
if (res.data.code === 0 && res.data.data) {
stats.todayCount = res.data.data.todayCount || 0
stats.weekCount = res.data.data.weekCount || 0
stats.monthCount = res.data.data.monthCount || 0
stats.pendingCount = res.data.data.pendingCount || 0
}
} catch (e) {
console.error('获取统计数据失败:', e)
}
}
// 获取门店列表
const fetchStoreList = async () => {
try {
storeLoading.value = true
const res = await request.get<ApiResult<ShopStore[]>>('/shop/shop-store', { status: 1 })
if (res.data.code === 0 && res.data.data) {
storeList.value = res.data.data
}
} catch (e) {
console.error('获取门店列表失败:', e)
} finally {
storeLoading.value = false
}
}
// 获取预约列表
const fetchData = async () => {
try {
loading.value = true
const params: any = {
page: pagination.current,
limit: pagination.pageSize
}
if (searchForm.storeId) {
params.storeId = searchForm.storeId
}
if (searchForm.status) {
params.status = searchForm.status
}
if (searchForm.keywords) {
params.keywords = searchForm.keywords
}
const res = await request.get<ApiResult<PageResult<ShopBooking>>>(
'/shop/shop-booking/admin/page',
{ params }
)
if (res.data.code === 0 && res.data.data) {
dataSource.value = res.data.data.list || []
pagination.total = res.data.data.count || 0
} else {
console.error('获取预约列表失败:', res.data.message)
}
} catch (e) {
console.error('获取预约列表失败:', e)
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.current = 1
fetchData()
fetchStats()
}
// 重置
const handleReset = () => {
searchForm.storeId = undefined
searchForm.status = ''
searchForm.keywords = ''
pagination.current = 1
fetchData()
fetchStats()
}
// 门店变更
const handleStoreChange = () => {
pagination.current = 1
fetchData()
fetchStats()
}
// 表格分页变化
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchData()
}
// 刷新
const reload = () => {
fetchData()
fetchStats()
}
// 查看详情
const viewDetail = (record: ShopBooking) => {
currentRecord.value = record
detailVisible.value = true
}
// 更新状态
const updateStatus = async (id: number, status: string) => {
try {
const res = await request.put<ApiResult<any>>(
`/shop/shop-booking/admin/status/${id}`,
null,
{ params: { status } }
)
if (res.data.code === 0) {
message.success('状态更新成功')
fetchData()
fetchStats()
} else {
message.error(res.data.message || '操作失败')
}
} catch (e) {
console.error('更新状态失败:', e)
message.error('操作失败')
}
}
onMounted(() => {
fetchStoreList()
fetchData()
fetchStats()
})
return {
loading,
storeLoading,
dataSource,
storeList,
stats,
searchForm,
pagination,
columns,
detailVisible,
currentRecord,
getPageTitle,
getStatusColor,
getStatusText,
formatDateTime,
handleSearch,
handleReset,
handleStoreChange,
handleTableChange,
reload,
viewDetail,
updateStatus
}
}
})
</script>
<style scoped>
.stats-row {
margin-bottom: 16px;
}
.stat-suffix {
font-size: 14px;
color: #999;
margin-left: 4px;
}
.booking-no {
font-family: monospace;
color: #666;
}
.price {
color: #0e932e;
font-weight: 500;
}
.mt-4 {
margin-top: 16px;
}
.search-container {
background: #fafafa;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<div class="app-container">
<el-card>
<div slot="header" class="clearfix">
<span>佣金记录管理</span>
<el-button
style="float: right"
type="primary"
@click="handleSettle"
>
手动结算
</el-button>
</div>
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="用户ID">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户ID"
clearable
style="width: 150px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="待结算" :value="0" />
<el-option label="已结算" :value="1" />
<el-option label="已失效" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="list" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="userId" label="会员ID" width="100" />
<el-table-column prop="orderNo" label="订单号" width="200" />
<el-table-column prop="subordinateId" label="下级ID" width="100" />
<el-table-column label="佣金金额" width="120">
<template slot-scope="scope">
¥{{ scope.row.commissionAmount }}
</template>
</el-table-column>
<el-table-column label="订单金额" width="120">
<template slot-scope="scope">
¥{{ scope.row.orderAmount }}
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="settleTime" label="结算时间" width="180" />
</el-table>
<!-- 分页 -->
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.page"
:limit.sync="queryParams.limit"
@pagination="getList"
/>
</el-card>
</div>
</template>
<script>
import { getCommissionRecordList, settleCommission } from '@/api/shop/shopCommissionRecord';
export default {
name: 'ShopCommissionRecord',
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: {
page: 1,
limit: 20,
userId: '',
status: ''
}
};
},
created() {
this.getList();
},
methods: {
async getList() {
this.loading = true;
try {
const res = await getCommissionRecordList(this.queryParams);
this.list = res.data.list || [];
this.total = res.data.total || 0;
} catch (error) {
console.error('查询失败', error);
} finally {
this.loading = false;
}
},
handleQuery() {
this.queryParams.page = 1;
this.getList();
},
resetQuery() {
this.queryParams = {
page: 1,
limit: 20,
userId: '',
status: ''
};
this.getList();
},
async handleSettle() {
this.$confirm('确认要手动结算佣金吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await settleCommission();
this.$message.success('结算完成');
this.getList();
} catch (error) {
console.error('结算失败', error);
}
}).catch(() => {});
},
getStatusType(status) {
const map = { 0: 'warning', 1: 'success', 2: 'info' };
return map[status] || 'info';
},
getStatusText(status) {
const map = { 0: '待结算', 1: '已结算', 2: '已失效' };
return map[status] || '未知';
}
}
};
</script>
<style scoped>
.search-form {
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,62 @@
<!-- 搜索表单 -->
<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-input-search
allow-clear
placeholder="小区名称"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopCommunityParam} from "@/api/shop/shopCommunity/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopCommunityParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopCommunityParam>({
keywords: '',
name: undefined,
code: undefined
});
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,209 @@
<!-- 编辑弹窗 -->
<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="name">
<a-input
allow-clear
placeholder="请输入小区名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="小区编号" name="code">
<a-input
allow-clear
placeholder="请输入小区编号"
v-model:value="form.code"
/>
</a-form-item>
<a-form-item label="详细地址" name="address">
<a-input
allow-clear
placeholder="请输入详细地址"
v-model:value="form.address"
/>
</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="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>
</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 { addShopCommunity, updateShopCommunity } from '@/api/shop/shopCommunity';
import { ShopCommunity } from '@/api/shop/shopCommunity/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?: ShopCommunity | 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<ShopCommunity>({
id: undefined,
name: undefined,
code: undefined,
address: undefined,
tenantId: undefined,
createTime: undefined,
shopCommunityId: undefined,
shopCommunityName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
shopCommunityName: [
{
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 ? updateShopCommunity : addShopCommunity;
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,252 @@
<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>
<!-- 编辑弹窗 -->
<ShopCommunityEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } 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 ShopCommunityEdit from './components/shopCommunityEdit.vue';
import { pageShopCommunity, removeShopCommunity, removeBatchShopCommunity } from '@/api/shop/shopCommunity';
import type { ShopCommunity, ShopCommunityParam } from '@/api/shop/shopCommunity/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopCommunity[]>([]);
// 当前编辑数据
const current = ref<ShopCommunity | 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 pageShopCommunity({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '小区名称',
dataIndex: 'name',
key: 'name',
ellipsis: true
},
{
title: '小区编号',
dataIndex: 'code',
key: 'code',
ellipsis: true
},
{
title: '详细地址',
dataIndex: 'address',
key: 'address',
ellipsis: true
},
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 120
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopCommunityParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopCommunity) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopCommunity) => {
const hide = message.loading('请求中..', 0);
removeShopCommunity(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);
removeBatchShopCommunity(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: ShopCommunity) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopCommunity'
};
</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>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,803 @@
<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-option :value="40">无门槛券</a-select-option>
<a-select-option :value="50">场地使用券</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>
<template v-else-if="record.type === 40">
<span class="value-amount" style="color: #1890ff"
>¥{{ Number(record.reducePrice).toFixed(2) || '0.00' }}</span
>
<div class="value-condition">无门槛</div>
</template>
<template v-else-if="record.type === 50">
<span class="value-free" style="color: #13c2c2">场地券</span>
<div class="value-condition">
{{ record.useCount === -1 ? '无限次' : record.useCount + '次' }}
<template v-if="record.useDuration"
>/{{ record.useDuration }}分钟</template
>
</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 === 'receiveTarget'">
<a-tag :color="getReceiveTargetColor(record.receiveTarget || 0)">
{{ getReceiveTargetText(record.receiveTarget || 0) }}
</a-tag>
<template v-if="record.receiveTarget === 3 && record.receiveUserIds">
<div style="color: #999; font-size: 12px">
已指定 {{ JSON.parse(record.receiveUserIds).length }}
</div>
</template>
</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: 'receiveTarget',
key: 'receiveTarget',
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: Record<number, string> = {
10: '满减券',
20: '折扣券',
30: '免费券',
40: '无门槛券',
50: '场地使用券'
};
return typeMap[type] || '未知';
};
const getCouponTypeColor = (type: number) => {
const colorMap: Record<number, string> = {
10: 'red',
20: 'orange',
30: 'green',
40: 'blue',
50: 'cyan'
};
return colorMap[type] || 'default';
};
const getReceiveTargetText = (target: number) => {
const targetMap: Record<number, string> = {
0: '全部用户',
1: '仅会员',
2: '仅非会员',
3: '指定用户'
};
return targetMap[target] || '全部';
};
const getReceiveTargetColor = (target: number) => {
const colorMap: Record<number, string> = {
0: 'default',
1: 'gold',
2: 'geekblue',
3: 'magenta'
};
return colorMap[target] || '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,221 @@
<!-- 搜索表单 -->
<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.realName"
placeholder="请输入申请人姓名"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="手机号码">
<a-input
v-model:value="searchForm.mobile"
placeholder="请输入手机号码"
allow-clear
style="width: 160px"
/>
</a-form-item>
<a-form-item label="申请方式">
<a-select
v-model:value="searchForm.applyType"
placeholder="全部方式"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">需要审核</a-select-option>
<a-select-option :value="20">免审核</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审核状态">
<a-select
v-model:value="searchForm.applyStatus"
placeholder="全部状态"
allow-clear
style="width: 120px"
>
<a-select-option :value="10">待审核</a-select-option>
<a-select-option :value="20">审核通过</a-select-option>
<a-select-option :value="30">审核驳回</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="申请时间">
<a-range-picker
v-model:value="searchForm.dateRange"
style="width: 240px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit" class="ele-btn-icon">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="resetSearch"> 重置 </a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-space>
<!-- <a-button type="primary" @click="add" class="ele-btn-icon">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- 新增申请-->
<!-- </a-button>-->
<a-button
type="primary"
ghost
:disabled="!selection?.length"
@click="batchApprove"
class="ele-btn-icon"
>
<template #icon>
<CheckOutlined />
</template>
批量通过
</a-button>
<a-button
:disabled="!selection?.length"
@click="exportData"
class="ele-btn-icon"
>
<template #icon>
<ExportOutlined />
</template>
导出数据
</a-button>
</a-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import {
PlusOutlined,
SearchOutlined,
CheckOutlined,
ExportOutlined
} from '@ant-design/icons-vue';
import type { ShopDealerApplyParam } from '@/api/shop/shopDealerApply/model';
import dayjs from 'dayjs';
const props = withDefaults(
defineProps<{
// 选中的数据
selection?: 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'
);
}
emit('search', searchParams);
};
// 重置搜索
const resetSearch = () => {
searchForm.realName = '';
searchForm.mobile = '';
searchForm.applyType = undefined;
searchForm.applyStatus = undefined;
searchForm.dateRange = undefined;
emit('search', {});
};
// 新增
const add = () => {
emit('add');
};
// 批量通过
const batchApprove = () => {
emit('batchApprove');
};
// 导出数据
const exportData = () => {
emit('export');
};
</script>
<style lang="less" scoped>
.search-container {
background: #fff;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
.search-form {
margin-bottom: 16px;
:deep(.ant-form-item) {
margin-bottom: 8px;
}
}
.action-buttons {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
}
</style>

View File

@@ -0,0 +1,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://cms-api.websoft.top/api/shop/shop-dealer-apply/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 { importShopDealerApplies } from '@/api/shop/shopDealerApply';
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;
importShopDealerApplies(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,298 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="600"
: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: 18 }"
>
<a-form-item label="企业名称" name="dealerName">
<a-input placeholder="请输入企业名称" v-model:value="form.dealerName" />
</a-form-item>
<a-form-item label="入市状态" name="applyStatus">
<a-select
v-model:value="form.applyStatus"
placeholder="请选择入市状态"
@change="handleStatusChange"
>
<a-select-option :value="10">
<a-tag>未入市</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>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { assignObject } from 'ele-admin-pro';
import {
addShopDealerApply,
updateShopDealerApply
} from '@/api/shop/shopDealerApply';
import { ShopDealerApply } from '@/api/shop/shopDealerApply/model';
import { FormInstance } from 'ant-design-vue/es/form';
// 是否是修改
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 maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 表单数据
const form = reactive<ShopDealerApply>({
applyId: undefined,
type: 3,
userId: undefined,
dealerName: '',
realName: '',
mobile: '',
refereeId: undefined,
applyType: 10,
applyTime: undefined,
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
dealerName: [
{
required: true,
message: '请输入经销商名称',
trigger: 'blur'
}
],
realName: [
{
required: true,
message: '请输入企业名称',
trigger: 'blur'
}
],
applyStatus: [
{
required: true,
message: '请选择审核状态',
trigger: 'change'
}
]
});
const { resetFields } = useForm(form, rules);
/* 处理审核状态变化 */
const handleStatusChange = (value: number) => {
// 当状态改为审核通过或驳回时,自动设置审核时间为当前时间
if ((value === 20 || value === 30) && !form.auditTime) {
form.auditTime = dayjs();
}
// 当状态改为待审核时,清空审核时间和驳回原因
if (value === 10) {
form.auditTime = undefined;
form.rejectReason = '';
}
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
// 动态验证规则
const validateFields: string[] = [
'userId',
'realName',
'mobile',
'applyStatus'
];
// 如果是驳回状态,需要验证驳回原因
if (form.applyStatus === 30) {
validateFields.push('rejectReason');
}
// 如果是审核通过或驳回状态,需要验证审核时间
if (form.applyStatus === 20 || form.applyStatus === 30) {
validateFields.push('auditTime');
}
formRef.value
.validate(validateFields)
.then(() => {
loading.value = true;
const formData = {
...form
};
// 处理时间字段转换 - 转换为ISO字符串格式
if (formData.applyTime) {
if (dayjs.isDayjs(formData.applyTime)) {
formData.applyTime = formData.applyTime.format(
'YYYY-MM-DD HH:mm:ss'
);
} else if (typeof formData.applyTime === 'number') {
formData.applyTime = dayjs(formData.applyTime).format(
'YYYY-MM-DD HH:mm:ss'
);
}
}
if (formData.auditTime) {
if (dayjs.isDayjs(formData.auditTime)) {
formData.auditTime = formData.auditTime.format(
'YYYY-MM-DD HH:mm:ss'
);
} else if (typeof formData.auditTime === 'number') {
formData.auditTime = dayjs(formData.auditTime).format(
'YYYY-MM-DD HH:mm:ss'
);
}
}
// 当审核状态为通过或驳回时,确保有审核时间
if (
(formData.applyStatus === 20 || formData.applyStatus === 30) &&
!formData.auditTime
) {
formData.auditTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
}
// 当状态为待审核时,清空审核时间
if (formData.applyStatus === 10) {
formData.auditTime = undefined;
}
const saveOrUpdate = isUpdate.value
? updateShopDealerApply
: addShopDealerApply;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
// 处理时间字段 - 确保转换为dayjs对象
if (props.data.applyTime) {
form.applyTime = dayjs(props.data.applyTime);
}
if (props.data.auditTime) {
form.auditTime = dayjs(props.data.auditTime);
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
applyId: undefined,
userId: undefined,
realName: '',
mobile: '',
refereeId: undefined,
applyType: 10,
applyTime: dayjs(),
applyStatus: 10,
auditTime: undefined,
rejectReason: '',
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
margin: 24px 0 16px 0;
.ant-divider-inner-text {
padding: 0 16px 0 0;
}
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-radio) {
display: flex;
align-items: center;
margin-bottom: 8px;
.ant-radio-inner {
margin-right: 8px;
}
}
:deep(.ant-select-selection-item) {
display: flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,301 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="applyId"
:columns="columns"
:datasource="datasource"
class="sys-org-table"
:scroll="{ x: 1300 }"
:where="defaultWhere"
:customRow="customRow"
cache-key="proSystemShopDealerApplyTable"
>
<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 === 'applyStatus'">
<span class="text-green-500" v-if="record.applyStatus == 20"
>已入市</span
>
<span class="text-gray-300" v-else>未入市</span>
</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>
<!-- 编辑弹窗 -->
<ShopDealerApplyEdit
v-model:visible="showEdit"
:data="current"
:organization-list="data"
@done="reload"
/>
<!-- 导入弹窗 -->
<ShopDealerApplyImport v-model:visible="showImport" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import { PlusOutlined, CloudUploadOutlined } 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 } from 'ele-admin-pro/es';
import ShopDealerApplyEdit from './components/shopDealerApplyEdit.vue';
import ShopDealerApplyImport from './components/shop-dealer-apply-import.vue';
import { toDateString } from 'ele-admin-pro';
import { utils, writeFile } from 'xlsx';
import dayjs from 'dayjs';
import { Organization } from '@/api/system/organization/model';
import { getPageTitle } from '@/utils/common';
import router from '@/router';
import {
listShopDealerApply,
pageShopDealerApply,
removeShopDealerApply
} from '@/api/shop/shopDealerApply';
import {
ShopDealerApply,
ShopDealerApplyParam
} from '@/api/shop/shopDealerApply/model';
// 加载状态
const loading = ref(true);
// 树形数据
const data = ref<Organization[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 表格选中数据
const selection = ref<ShopDealerApply[]>([]);
// 当前编辑数据
const current = ref<ShopDealerApply | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
// 导出加载状态
const exportLoading = ref(false);
const searchText = ref('');
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
// {
// title: 'ID',
// dataIndex: 'userId',
// width: 90,
// showSorterTooltip: false
// },
{
title: '企业名称',
dataIndex: 'dealerName',
align: 'dealerName',
showSorterTooltip: false
},
{
title: '入市情况',
dataIndex: 'applyStatus',
key: 'applyStatus',
align: 'center',
sorter: true
},
{
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 }) => {
where = {};
where.keywords = searchText.value;
where.type = 3;
return pageShopDealerApply({ page, limit, ...where, ...orders });
};
/* 搜索 */
const reload = (where?: ShopDealerApplyParam) => {
selection.value = [];
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerApply) => {
current.value = row ?? null;
showEdit.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 listShopDealerApply(params);
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
exportLoading.value = false;
return;
}
// 将数据转换为Excel行
list.forEach((user: ShopDealerApply) => {
array.push([
`${user.applyId || ''}`,
`${user.realName || ''}`,
`${user.mobile || ''}`,
`${user.applyStatus === 20 ? '通过' : '未通过'}`,
`${user.createTime || ''}`
]);
});
// 生成Excel文件
const sheetName = `导出企业入市${dayjs(new Date()).format('YYYYMMDD')}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [];
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 remove = (row: ShopDealerApply) => {
const hide = messageLoading('请求中..', 0);
removeShopDealerApply(row.applyId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 自定义行属性 */
const customRow = (record: ShopDealerApply) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
watch(
() => router.currentRoute.value.query,
() => {},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'ShopDealerApplyRs'
};
</script>

View File

@@ -0,0 +1,117 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="用户ID|订单编号"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
<a-button type="dashed" @click="handleExport">导出xls</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 { pageShopDealerCapital } from '@/api/shop/shopDealerCapital';
import {
ShopDealerCapital,
ShopDealerCapitalParam
} from '@/api/shop/shopDealerCapital/model';
import { getTenantId } from '@/utils/domain';
import useSearch from '@/utils/use-search';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerCapitalParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerCapitalParam>({
keywords: '',
userId: undefined,
toUserId: undefined,
limit: 5000
});
const list = ref<ShopDealerCapital[]>([]);
// 导出
const handleExport = async () => {
const array: (string | number)[][] = [
[
'订单号',
'用户',
'收益类型',
'金额',
'描述',
'创建时间',
'租户ID'
]
];
// 按搜索结果导出
await pageShopDealerCapital(where)
.then((data) => {
list.value = data?.list || [];
list.value?.forEach((d: ShopDealerCapital) => {
array.push([
`${d.orderNo}`,
`${d.nickName}(${d.userId})`,
`${d.flowType == 10 ? '佣金收入' : ''}`,
`${d.money}`,
`${d.comments}`,
`${d.createTime}`,
`${d.tenantId}`
]);
});
const sheetName = `bak_shop_dealer_capital_${getTenantId()}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 10 },
{ wch: 20 },
{ wch: 20 },
{ wch: 15 },
{ wch: 10 },
{ wch: 10 },
{ wch: 20 }
];
message.loading('正在导出...');
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
}, 1000);
})
.catch((msg) => {
message.error(msg);
})
.finally(() => {});
};
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,326 @@
<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 === 'userId'">
<div>{{ record.nickName }}({{ record.userId }})</div>
</template>
<template v-if="column.key === 'orderStatus'">
<a-space :size="6" wrap>
<!-- 订单状态 -->
<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
>
</a-space>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</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 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[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '订单号',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center',
customRender: ({ text }) => text || '-'
},
{
title: '用户',
dataIndex: 'userId',
key: 'userId',
align: 'center',
fixed: 'left'
},
{
title: '收益类型',
dataIndex: 'flowType',
key: 'flowType',
align: 'center',
customRender: ({ text }) => {
const typeMap = {
10: { text: '佣金收入', color: 'success' },
20: { text: '提现支出', color: 'warning' },
30: { text: '转账支出', color: 'error' },
40: { text: '转账收入', color: 'processing' },
50: { 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',
customRender: ({ text, record }) => {
const amount = parseFloat(text || '0').toFixed(2);
const isIncome =
record.flowType === 10 ||
record.flowType === 40 ||
record.flowType === 50;
return {
type: 'span',
props: {
style: {
color: isIncome ? '#424242' : '#ff4d4f'
}
},
children: `${isIncome ? '' : '-'} ${amount}`
};
}
},
// {
// title: '对方用户',
// dataIndex: 'toUserId',
// key: 'toUserId',
// align: 'center',
// customRender: ({ text }) => (text ? `ID: ${text}` : '-')
// },
{
title: '描述',
dataIndex: 'comments',
key: 'comments',
align: 'left',
ellipsis: true,
customRender: ({ text }) => text || '-'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true
},
{
title: '订单状态',
dataIndex: 'orderStatus',
key: 'orderStatus',
align: 'center',
width: 90
}
// {
// 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,79 @@
<!-- 经销商订单导入弹窗 -->
<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>
</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 { importShopDealerOrder } from '@/api/shop/shopDealerOrder';
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;
importShopDealerOrder(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,147 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="客户名称|订单编号"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
<a-button type="dashed" @click="handleExport">导出xls</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { utils, writeFile } from 'xlsx';
import { message } from 'ant-design-vue';
import { getTenantId } from '@/utils/domain';
import useSearch from '@/utils/use-search';
import type {
ShopDealerOrder,
ShopDealerOrderParam
} from '@/api/shop/shopDealerOrder/model';
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopDealerOrderParam): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerOrderParam>({
keywords: '',
userId: undefined,
orderNo: undefined,
isSettled: 1, // 与列表页一致:只展示/导出已结算订单
page: 1,
limit: 5000
});
const list = ref<ShopDealerOrder[]>([]);
const toMoney = (val: unknown) => {
const n = Number.parseFloat(String(val ?? '0'));
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
};
const toDateTime = (val: unknown) => {
if (val === null || val === undefined || val === '') return '';
if (typeof val === 'number') return new Date(val).toLocaleString();
return String(val);
};
// 导出
const handleExport = async () => {
const array: (string | number)[][] = [
[
// 与 `src/views/shop/shopDealerOrder/index.vue` 表头保持一致
'订单编号',
'买家',
'订单金额',
'一级佣金(10%)',
'二级佣金(5%)',
'一级门店分红(2%/3%)',
'二级门店分红(1%)',
'结算状态',
'创建时间'
]
];
// 按搜索结果导出
await pageShopDealerOrder(where)
.then((data) => {
list.value = data?.list || [];
list.value?.forEach((d: ShopDealerOrder) => {
const buyer =
(d.title ? String(d.title) : '') +
(d.nickname || d.userId
? `\n${d.nickname ?? '-'}(${d.userId ?? '-'})`
: '');
const firstDividendUserName = (d as any)?.firstDividendUserName ?? '-';
const firstDividend = (d as any)?.firstDividend ?? 0;
const secondDividendUserName =
(d as any)?.secondDividendUserName ?? '-';
const secondDividend = (d as any)?.secondDividend ?? 0;
array.push([
d.orderNo ?? '',
buyer,
toMoney(d.orderPrice),
`${toMoney(d.firstMoney)}\n${d.firstNickname ?? '-'}`,
`${toMoney(d.secondMoney)}\n${d.secondNickname ?? '-'}`,
`${toMoney(firstDividend)}\n${firstDividendUserName}`,
`${toMoney(secondDividend)}\n${secondDividendUserName}`,
d.isSettled === 1 ? '已结算' : '未结算',
`${d.createTime ?? ''}${
d.settleTime ? `\n${toDateTime(d.settleTime)}` : ''
}`
]);
});
const sheetName = `bak_shop_dealer_order_${getTenantId()}`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{ wch: 20 },
{ wch: 20 },
{ wch: 12 },
{ wch: 16 },
{ wch: 16 },
{ wch: 18 },
{ wch: 16 },
{ wch: 10 },
{ wch: 20 }
];
message.loading('正在导出...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
message.destroy();
}, 1000);
})
.catch((msg) => {
message.destroy();
message.error(msg);
})
.finally(() => {});
};
void props;
</script>

View File

@@ -0,0 +1,333 @@
<!-- 编辑弹窗 -->
<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="orderNo">
{{ form.orderNo }}
</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">一级佣金10%</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">
{{ '10%' }}
</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 { updateShopDealerOrder } from '@/api/shop/shopDealerOrder';
// 是否是修改
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
};
updateShopDealerOrder(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,579 @@
<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"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'title'">
<div>{{ record.title }}</div>
<div class="text-gray-400"
>{{ record.nickname }}({{ record.userId }})</div
>
</template>
<template v-if="column.key === 'orderPrice'">
{{ parseFloat(record.orderPrice).toFixed(2) }}
</template>
<template v-if="column.key === 'degreePrice'">
{{ record.degreePrice.toFixed(2) }}
</template>
<template v-if="column.key === 'price'">
{{ record.price || 0 }}
</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 === 'firstNickname'">
<div>{{ record.firstMoney }}</div>
<div class="text-gray-400">{{ record.firstNickname || '-' }}</div>
</template>
<template v-if="column.key === 'secondNickname'">
<div>{{ record.secondMoney }}</div>
<div class="text-gray-400">{{ record.secondNickname || '-' }}</div>
</template>
<template v-if="column.key === 'firstDividendUserName'">
<div>{{ record.firstDividend }}</div>
<div class="text-gray-400"
>{{ record.firstDividendUserName || '-' }}</div
>
</template>
<template v-if="column.key === 'secondDividendUserName'">
<div>{{ record.secondDividend }}</div>
<div class="text-gray-400"
>{{ record.secondDividendUserName || '-' }}</div
>
</template>
<template v-if="column.key === 'dealerInfo'">
<div class="dealer-info">
<div v-if="record.firstUserId" class="dealer-level">
<a-tag color="red">一级</a-tag>
用户{{ record.firstUserId }} - ¥{{
parseFloat(record.firstMoney || '0').toFixed(2)
}}
</div>
<div v-if="record.secondUserId" class="dealer-level">
<a-tag color="orange">二级</a-tag>
用户{{ record.secondUserId }} - ¥{{
parseFloat(record.secondMoney || '0').toFixed(2)
}}
</div>
<div v-if="record.thirdUserId" class="dealer-level">
<a-tag color="gold">三级</a-tag>
用户{{ record.thirdUserId }} - ¥{{
parseFloat(record.thirdMoney || '0').toFixed(2)
}}
</div>
</div>
</template>
<template v-if="column.key === 'isInvalid'">
<a-tag v-if="record.isInvalid === 0" color="success">已签约</a-tag>
<a-tag v-if="record.isInvalid === 1" color="error">未签约</a-tag>
</template>
<template v-if="column.key === 'isSettled'">
<div class="flex flex-col items-start gap-1">
<a-tag v-if="record.isSettled === 0" color="orange">未结算</a-tag>
<a-tag v-if="record.isSettled === 1" color="purple">已结算</a-tag>
<a-tag v-if="record.isUnfreeze === 1" color="success">已解冻</a-tag>
<a-button
v-if="record.isSettled === 1 && record.isUnfreeze !== 1"
type="link"
size="small"
danger
@click="handleManualUnfreeze(record)"
>
<thunderbolt-outlined />
手动解冻
</a-button>
</div>
</template>
<template v-if="column.key === 'orderStatus'">
<a-space :size="6" wrap>
<!-- 订单状态 -->
<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
>
</a-space>
</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>
<a-tooltip title="解冻时间">
<span class="text-green-500">{{ record.unfreezeTime }}</span>
</a-tooltip>
</div>
</template>
<template v-if="column.key === 'action'">
<template v-if="record.isSettled === 0 && record.isInvalid === 0">
<a @click="settleOrder(record)" class="ele-text-success">
结算
</a>
<a-divider type="vertical" />
</template>
<!-- <template v-if="record.isInvalid === 0">-->
<!-- <a-popconfirm-->
<!-- title="确定要标记此订单为失效吗?"-->
<!-- @confirm="invalidateOrder(record)"-->
<!-- placement="topRight"-->
<!-- >-->
<!-- <a class="text-purple-500">-->
<!-- 验证-->
<!-- </a>-->
<!-- </a-popconfirm>-->
<!-- </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,
DollarOutlined,
ThunderboltOutlined
} 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 {
updateShopDealerOrder
} from '@/api/shop/shopDealerOrder';
import { unfreezeShopDealerOrder } from '@/api/shop/shopDealerOrder';
// 表格实例
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 datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
// 已结算订单
where.isSettled = 1;
return pageShopDealerOrder({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '订单编号',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center',
width: 200
},
{
title: '买家',
dataIndex: 'title',
key: 'title'
},
{
title: '订单金额',
dataIndex: 'orderPrice',
key: 'orderPrice',
align: 'center'
},
{
title: '一级佣金',
dataIndex: 'firstNickname',
key: 'firstNickname',
align: 'center'
},
{
title: '二级佣金',
dataIndex: 'secondNickname',
key: 'secondNickname',
align: 'center'
},
{
title: '一级门店分红',
dataIndex: 'firstDividendUserName',
key: 'firstDividendUserName',
align: 'center'
},
{
title: '二级门店分红',
dataIndex: 'secondDividendUserName',
key: 'secondDividendUserName',
align: 'center'
},
{
title: '结算状态',
dataIndex: 'isSettled',
key: 'isSettled',
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 180
},
{
title: '订单状态',
dataIndex: 'orderStatus',
key: 'orderStatus',
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 handleManualUnfreeze = (row: ShopDealerOrder) => {
Modal.confirm({
title: '手动解冻',
content: `确定要手动解冻订单 ${row.orderNo} 的佣金吗?系统将执行与定时任务相同的条件检查。`,
icon: createVNode(ThunderboltOutlined),
okText: '确认解冻',
okType: 'primary',
cancelText: '取消',
onOk: async () => {
const hide = message.loading('正在执行手动解冻...', 0);
try {
const detail = await unfreezeShopDealerOrder(row.orderNo!);
hide();
Modal.success({
title: '解冻执行完成',
content: detail
? `${detail.split('\n').filter(l => l.trim()).map(l => `<div style="font-size:12px;color:#666">${l}</div>`).join('')}`
: '操作成功',
width: 600,
okText: '知道了'
});
reload();
} catch (e: any) {
hide();
Modal.error({
title: '解冻失败',
content: e.message || '未知错误',
okText: '知道了'
});
}
}
});
};
/* 结算单个订单 */
const settleOrder = (row: ShopDealerOrder) => {
const totalCommission = (
parseFloat(row.firstMoney || '0') +
parseFloat(row.secondMoney || '0') +
parseFloat(row.thirdMoney || '0')
).toFixed(2);
Modal.confirm({
title: '确认结算',
content: `确定要结算此订单吗?总佣金金额:¥${totalCommission}`,
icon: createVNode(DollarOutlined),
okText: '确认结算',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在结算...', 0);
// 这里调用结算API
updateShopDealerOrder({
...row,
isSettled: 1
});
setTimeout(() => {
hide();
message.success('结算成功');
reload();
}, 1000);
}
});
};
/* 批量结算 */
const batchSettle = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
const validOrders = selection.value.filter(
(order) => order.isSettled === 0 && order.isInvalid === 0
);
if (!validOrders.length) {
message.error('所选订单中没有可结算的订单');
return;
}
const totalCommission = validOrders
.reduce((sum, order) => {
return (
sum +
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
);
}, 0)
.toFixed(2);
Modal.confirm({
title: '批量结算确认',
content: `确定要结算选中的 ${validOrders.length} 个订单吗?总佣金金额:¥${totalCommission}`,
icon: createVNode(ExclamationCircleOutlined),
okText: '确认结算',
okType: 'primary',
cancelText: '取消',
onOk: () => {
const hide = message.loading('正在批量结算...', 0);
// 这里调用批量结算API
setTimeout(() => {
hide();
message.success(`成功结算 ${validOrders.length} 个订单`);
reload();
}, 1500);
}
});
};
/* 打开编辑弹窗 */
const 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,625 @@
<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 {
addShopDealerSetting,
updateShopDealerSetting,
listShopDealerSetting
} from '@/api/shop/shopDealerSetting';
import type { ShopDealerSetting } from '@/api/shop/shopDealerSetting/model';
// 当前激活的标签页
const activeTab = ref('basic');
// 保存状态
const saving = ref(false);
const settingMap = ref<Record<string, ShopDealerSetting>>({});
const settingValuesMap = ref<Record<string, any>>({});
const settingMeta = {
basic: '基础设置',
condition: '分销商条件',
settlement: '结算',
license: '申请协议',
words: '自定义文字',
background: '页面背景图'
};
// 基础设置
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 getBooleanFlag = (value: any) => value === 1 || value === true;
const getNumberFlag = (value: any) => (value ? 1 : 0);
const pickEnumValue = (value: any, options: number[], fallback: number) =>
options.includes(value) ? value : fallback;
const ensurePath = (root: Record<string, any>, path: string[]) => {
let current = root;
path.forEach((key) => {
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
});
return current;
};
const setWordValue = (
root: Record<string, any>,
path: string[],
value: string
) => {
const node = ensurePath(root, path);
if (typeof node.default !== 'string') {
node.default = value;
}
node.value = value;
};
const getUploadUrl = (fileList: any[]) => {
if (!Array.isArray(fileList) || fileList.length === 0) {
return '';
}
const file = fileList[0];
return (
file?.url ||
file?.thumbUrl ||
file?.response?.url ||
file?.response?.data?.url ||
file?.response?.data?.path ||
''
);
};
const toUploadFileList = (url?: string) => {
if (!url) {
return [];
}
return [
{
uid: 'background-0',
name: '背景图',
status: 'done',
url
}
];
};
/* 图片预览 */
const handlePreview = (file: any) => {
console.log('预览图片:', file);
};
/* 加载设置 */
const loadSettings = async () => {
try {
const list = await listShopDealerSetting();
const nextMap: Record<string, ShopDealerSetting> = {};
const nextValues: Record<string, any> = {};
list.forEach((item) => {
if (!item.key) {
return;
}
nextMap[item.key] = item;
if (item.values) {
try {
nextValues[item.key] = JSON.parse(item.values);
} catch (error) {
console.warn('解析设置失败:', item.key, error);
}
}
});
settingMap.value = nextMap;
settingValuesMap.value = nextValues;
const basicValue = nextValues.basic || {};
basicSettings.enableDistribution = getBooleanFlag(basicValue.is_open);
basicSettings.distributionLevel = basicValue.level ?? 3;
basicSettings.dealerSelfBuy = getBooleanFlag(basicValue.self_buy);
const conditionValue = nextValues.condition || {};
commissionSettings.applyType = pickEnumValue(
conditionValue.become,
[10, 20],
commissionSettings.applyType
);
const settlementValue = nextValues.settlement || {};
commissionSettings.settlementType = pickEnumValue(
settlementValue.settle_days ?? settlementValue.settlement_type,
[10, 20],
commissionSettings.settlementType
);
commissionSettings.minWithdrawAmount =
settlementValue.min_money ?? commissionSettings.minWithdrawAmount;
withdrawSettings.withdrawMethods = Array.isArray(settlementValue.pay_type)
? settlementValue.pay_type
: withdrawSettings.withdrawMethods;
withdrawSettings.withdrawFeeRate =
typeof settlementValue.fee_rate === 'number'
? settlementValue.fee_rate
: withdrawSettings.withdrawFeeRate;
withdrawSettings.withdrawAudit =
settlementValue.audit === undefined
? withdrawSettings.withdrawAudit
: getBooleanFlag(settlementValue.audit);
const licenseValue = nextValues.license || {};
if (licenseValue.license) {
agreementSettings.dealerAgreement = licenseValue.license;
}
const wordsValue = nextValues.words || {};
if (wordsValue.index?.title?.value) {
pageSettings.centerTitle = wordsValue.index.title.value;
}
if (wordsValue.apply?.words?.wait_audit?.value) {
notificationSettings.applySuccessText =
wordsValue.apply.words.wait_audit.value;
}
if (wordsValue.apply?.words?.fail?.value) {
notificationSettings.applyFailText = wordsValue.apply.words.fail.value;
}
if (wordsValue.withdraw_apply?.words?.success?.value) {
notificationSettings.withdrawSuccessText =
wordsValue.withdraw_apply.words.success.value;
}
const backgroundValue = nextValues.background || {};
const backgroundUrl =
backgroundValue.index ||
backgroundValue.apply ||
backgroundValue.withdraw_apply;
pageSettings.backgroundImages = toUploadFileList(backgroundUrl);
} catch (error) {
console.error('加载设置失败:', error);
message.error('加载设置失败');
}
};
const saveSettingItem = async (
key: keyof typeof settingMeta,
values: Record<string, any>
) => {
const existSetting = settingMap.value[key];
const payload: ShopDealerSetting = {
...(existSetting || {}),
key,
describe: existSetting?.describe ?? settingMeta[key],
values: JSON.stringify(values),
updateTime: Date.now()
};
const saveOrUpdate = existSetting
? updateShopDealerSetting
: addShopDealerSetting;
await saveOrUpdate(payload);
settingMap.value[key] = payload;
settingValuesMap.value[key] = values;
};
/* 保存设置 */
const saveSettings = async () => {
saving.value = true;
try {
const basicValues = {
is_open: getNumberFlag(basicSettings.enableDistribution),
level: basicSettings.distributionLevel,
self_buy: getNumberFlag(basicSettings.dealerSelfBuy)
};
const conditionValues = {
...(settingValuesMap.value.condition || {}),
become: commissionSettings.applyType
};
const settlementValues = {
...(settingValuesMap.value.settlement || {}),
pay_type: [...withdrawSettings.withdrawMethods],
min_money: commissionSettings.minWithdrawAmount,
settle_days: commissionSettings.settlementType,
fee_rate: withdrawSettings.withdrawFeeRate,
audit: getNumberFlag(withdrawSettings.withdrawAudit)
};
const licenseValues = {
license: agreementSettings.dealerAgreement
};
const wordsValues = {
...(settingValuesMap.value.words || {})
};
setWordValue(wordsValues, ['index', 'title'], pageSettings.centerTitle);
setWordValue(
wordsValues,
['apply', 'words', 'wait_audit'],
notificationSettings.applySuccessText
);
setWordValue(
wordsValues,
['apply', 'words', 'fail'],
notificationSettings.applyFailText
);
setWordValue(
wordsValues,
['withdraw_apply', 'words', 'success'],
notificationSettings.withdrawSuccessText
);
const backgroundUrl = getUploadUrl(pageSettings.backgroundImages);
const backgroundValues = {
...(settingValuesMap.value.background || {}),
index: backgroundUrl || settingValuesMap.value.background?.index || '',
apply: backgroundUrl || settingValuesMap.value.background?.apply || '',
withdraw_apply:
backgroundUrl || settingValuesMap.value.background?.withdraw_apply || ''
};
await saveSettingItem('basic', basicValues);
await saveSettingItem('condition', conditionValues);
await saveSettingItem('settlement', settlementValues);
await saveSettingItem('license', licenseValues);
await saveSettingItem('words', wordsValues);
await saveSettingItem('background', backgroundValues);
await loadSettings();
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,51 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="姓名|电话"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopDealerUserParam} from "@/api/shop/shopDealerUser/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerUserParam>({
keywords: '',
userId: undefined,
mobile: undefined,
realName: undefined
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,443 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
: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-divider orientation="left">
<span style="color: #1890ff; font-weight: 600">基础信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="24" v-if="!isUpdate">
<a-form-item label="关联用户" name="userId">
<SelectUser
:key="selectedUserText"
:value="selectedUserText"
:placeholder="`选择用户`"
@done="onChooseUser"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="form.type" placeholder="请选择类型">
<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-col>
<a-col :span="12">
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
:disabled="true"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="真实姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item-->
<!-- label="支付密码"-->
<!-- name="payPassword"-->
<!-- :help="isUpdate ? '留空表示不修改' : undefined"-->
<!-- >-->
<!-- <a-input-password-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入支付密码"-->
<!-- v-model:value="form.payPassword"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<a-col :span="12">
<a-form-item label="头像" name="image">
<a-image :src="form.avatar" :width="50" :preview="false" />
</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="money">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.money"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="已冻结" name="freezeMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.freezeMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="累积提现" name="totalMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.totalMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="佣金比例" name="rate">
<a-input-number
class="ele-fluid"
:min="0"
:max="1"
:precision="3"
stringMode
:disabled="true"
placeholder="例如 0.007"
v-model:value="form.rate"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="单价" name="price">-->
<!-- <a-input-number-->
<!-- class="ele-fluid"-->
<!-- :min="0"-->
<!-- :precision="2"-->
<!-- stringMode-->
<!-- placeholder="0.00"-->
<!-- v-model:value="form.price"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16">-->
<!-- <a-col :span="12">-->
<!-- <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-col>-->
<!-- <a-col :span="12">-->
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="3"-->
<!-- :maxlength="200"-->
<!-- show-count-->
<!-- placeholder="请输入备注"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- </a-row>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, toDateString, uuid } from 'ele-admin-pro';
import { addShopDealerUser, updateShopDealerUser } from '@/api/shop/shopDealerUser';
import { ShopDealerUser } from '@/api/shop/shopDealerUser/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import type { User } from '@/api/system/user/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerUser | 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<ShopDealerUser>({
id: undefined,
userId: undefined,
avatar: undefined,
type: 0,
realName: undefined,
mobile: undefined,
payPassword: '',
money: undefined,
freezeMoney: undefined,
totalMoney: undefined,
rate: undefined,
price: undefined,
refereeId: undefined,
firstNum: undefined,
secondNum: undefined,
thirdNum: undefined,
qrcode: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const createTimeText = computed(() => {
return form.createTime ? toDateString(form.createTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const updateTimeText = computed(() => {
return form.updateTime ? toDateString(form.updateTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const selectedUserText = ref<string>('');
// 表单验证规则
const rules = reactive({
userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请选择关联用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择类型',
trigger: 'change'
}
],
realName: [
{
required: true,
type: 'string',
message: '请填写真实姓名',
trigger: 'blur'
}
],
// mobile: [
// {
// required: true,
// type: 'string',
// message: '请填写手机号',
// trigger: 'blur'
// },
// {
// pattern: /^1\\d{10}$/,
// message: '手机号格式不正确',
// trigger: 'blur'
// }
// ],
payPassword: [
{
validator: (_rule: unknown, value: string) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请输入支付密码'));
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
return;
}
form.userId = user.userId;
// 新增时尽量帮用户填充,避免重复输入
if (!form.realName) {
form.realName = user.realName ?? user.nickname;
}
if (!form.mobile) {
form.mobile = user.phone ?? user.mobile;
}
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
};
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;
// 不在弹窗里编辑的字段不提交避免误更新如自增ID、删除标识等
const {
isDelete,
tenantId,
createTime,
updateTime,
...rest
} = form;
const formData: ShopDealerUser = { ...rest };
// userId 新增需要,编辑不允许修改
if (isUpdate.value) {
delete formData.userId;
}
// 编辑时留空表示不修改密码
if (isUpdate.value && !formData.payPassword) {
delete formData.payPassword;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
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) {
formRef.value?.clearValidate();
images.value = [];
if (props.data) {
assignObject(form, props.data);
// 不回显密码,避免误操作
form.payPassword = '';
selectedUserText.value = '';
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
// 新增时确保表单是干净的默认值
resetFields();
form.payPassword = '';
form.type = 0;
form.status = 0;
form.comments = '';
form.sortNumber = 100;
selectedUserText.value = '';
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
images.value = [];
selectedUserText.value = '';
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,356 @@
<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 === 'type'">
<a-tag v-if="record.type === 0">分销商</a-tag>
<a-tag v-if="record.type === 1" color="orange">门店</a-tag>
<a-tag v-if="record.type === 2" color="purple">总分销商</a-tag>
</template>
<template v-if="column.key === 'qrcode'">
<QrcodeOutlined :style="{fontSize: '24px'}" @click="openQrCode(record)" />
</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>
<!-- 编辑弹窗 -->
<ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 二维码预览 -->
<a-modal
v-model:visible="showQrModal"
:title="qrModalTitle"
:footer="null"
:width="380"
centered
destroy-on-close
>
<div style="display: flex; justify-content: center">
<a-image v-if="qrModalUrl" :src="qrModalUrl" :width="280" :preview="false" />
</div>
<div style="display: flex; justify-content: center; margin-top: 12px">
<a-space>
<a-button @click="copyQrUrl">复制链接</a-button>
<a-button type="primary" @click="openQrInNewTab">打开原图</a-button>
</a-space>
</div>
</a-modal>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, QrcodeOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据
const current = ref<ShopDealerUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示二维码弹窗
const showQrModal = ref(false);
const qrModalUrl = ref<string>('');
const qrModalTitle = ref<string>('二维码');
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
const getQrCodeUrl = (userId?: number) => {
return `https://guilixu-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${userId ?? ''}`;
};
const openQrCode = (row: ShopDealerUser) => {
if (!row.userId) {
message.warning('缺少用户ID无法生成二维码');
return;
}
qrModalUrl.value = getQrCodeUrl(row.userId);
qrModalTitle.value = row.realName ? `${row.realName} 的二维码` : `UID_${row.userId} 二维码`;
showQrModal.value = true;
};
const copyQrUrl = async () => {
if (!qrModalUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(qrModalUrl.value);
message.success('已复制');
} catch (e) {
message.error('复制失败,请手动复制');
}
};
const openQrInNewTab = () => {
if (!qrModalUrl.value) {
return;
}
window.open(qrModalUrl.value, '_blank');
};
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageShopDealerUser({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 120
},
{
title: '真实姓名',
dataIndex: 'realName',
key: 'realName'
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
{
title: '可提现',
dataIndex: 'money',
key: 'money',
width: 120
},
{
title: '已冻结',
dataIndex: 'freezeMoney',
key: 'freezeMoney',
width: 120
},
{
title: '累积提现',
dataIndex: 'totalMoney',
key: 'totalMoney',
width: 120
},
// {
// title: '推荐人用户ID',
// dataIndex: 'refereeId',
// key: 'refereeId',
// width: 120
// },
// {
// title: '成员数量(一级)',
// dataIndex: 'firstNum',
// key: 'firstNum',
// width: 120
// },
// {
// title: '成员数量(二级)',
// dataIndex: 'secondNum',
// key: 'secondNum',
// width: 120
// },
// {
// title: '成员数量(三级)',
// dataIndex: 'thirdNum',
// key: 'thirdNum',
// width: 120
// },
{
title: '专属二维码',
dataIndex: 'qrcode',
key: 'qrcode',
align: 'center'
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopDealerUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(row.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);
removeBatchShopDealerUser(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: ShopDealerUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerUser'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,51 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="姓名|电话"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopDealerUserParam} from "@/api/shop/shopDealerUser/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerUserParam>({
keywords: '',
userId: undefined,
mobile: undefined,
realName: undefined
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,468 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
: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-row :gutter="16">
<a-col :span="12">
<a-form-item label="绑定门店" name="shopName">
<a-input
allow-clear
placeholder="店铺名称"
:disabled="true"
v-model:value="form.shopName"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="小区名称" name="community">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入小区名称"-->
<!-- v-model:value="form.community"-->
<!-- />-->
<!-- </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="24" v-if="!isUpdate">
<a-form-item label="关联用户" name="userId">
<SelectUser
:key="selectedUserText"
:value="selectedUserText"
:placeholder="`选择用户`"
@done="onChooseUser"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="form.type" :disabled="true" placeholder="请选择类型">
<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-col>
<a-col :span="12">
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
:disabled="true"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="真实姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
:disabled="true"
placeholder="请输入真实姓名"
v-model:value="form.realName"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item-->
<!-- label="支付密码"-->
<!-- name="payPassword"-->
<!-- :help="isUpdate ? '留空表示不修改' : undefined"-->
<!-- >-->
<!-- <a-input-password-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入支付密码"-->
<!-- v-model:value="form.payPassword"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<a-col :span="12">
<a-form-item label="头像" name="image">
<a-image :src="form.avatar" :width="50" :preview="false" />
</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="money">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.money"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="已冻结" name="freezeMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.freezeMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="累积提现" name="totalMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.totalMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="佣金比例" name="rate">
<a-input-number
class="ele-fluid"
:min="0"
:max="1"
:precision="4"
stringMode
:disabled="true"
placeholder="例如 0.007"
v-model:value="form.rate"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="单价" name="price">-->
<!-- <a-input-number-->
<!-- class="ele-fluid"-->
<!-- :min="0"-->
<!-- :precision="2"-->
<!-- stringMode-->
<!-- placeholder="0.00"-->
<!-- v-model:value="form.price"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16">-->
<!-- <a-col :span="12">-->
<!-- <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-col>-->
<!-- <a-col :span="12">-->
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="3"-->
<!-- :maxlength="200"-->
<!-- show-count-->
<!-- placeholder="请输入备注"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- </a-row>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, toDateString, uuid } from 'ele-admin-pro';
import { addShopDealerUser, updateShopDealerUser } from '@/api/shop/shopDealerUser';
import { ShopDealerUser } from '@/api/shop/shopDealerUser/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import type { User } from '@/api/system/user/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerUser | 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<ShopDealerUser>({
id: undefined,
userId: undefined,
avatar: undefined,
type: 0,
realName: undefined,
mobile: undefined,
payPassword: '',
money: undefined,
freezeMoney: undefined,
totalMoney: undefined,
rate: undefined,
price: undefined,
refereeId: undefined,
firstNum: undefined,
secondNum: undefined,
thirdNum: undefined,
shopName: undefined,
qrcode: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const createTimeText = computed(() => {
return form.createTime ? toDateString(form.createTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const updateTimeText = computed(() => {
return form.updateTime ? toDateString(form.updateTime, 'yyyy-MM-dd HH:mm:ss') : '';
});
const selectedUserText = ref<string>('');
// 表单验证规则
const rules = reactive({
userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请选择关联用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择类型',
trigger: 'change'
}
],
dealerName: [
{
required: true,
type: 'string',
message: '请填写真实姓名',
trigger: 'blur'
}
],
// mobile: [
// {
// required: true,
// type: 'string',
// message: '请填写手机号',
// trigger: 'blur'
// },
// {
// pattern: /^1\\d{10}$/,
// message: '手机号格式不正确',
// trigger: 'blur'
// }
// ],
payPassword: [
{
validator: (_rule: unknown, value: string) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请输入支付密码'));
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
return;
}
form.userId = user.userId;
// 新增时尽量帮用户填充,避免重复输入
if (!form.realName) {
form.realName = user.realName ?? user.nickname;
}
if (!form.mobile) {
form.mobile = user.phone ?? user.mobile;
}
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
};
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;
// 不在弹窗里编辑的字段不提交避免误更新如自增ID、删除标识等
const {
isDelete,
tenantId,
createTime,
updateTime,
...rest
} = form;
const formData: ShopDealerUser = { ...rest };
// userId 新增需要,编辑不允许修改
if (isUpdate.value) {
delete formData.userId;
}
// 编辑时留空表示不修改密码
if (isUpdate.value && !formData.payPassword) {
delete formData.payPassword;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
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) {
formRef.value?.clearValidate();
images.value = [];
if (props.data) {
assignObject(form, props.data);
// 不回显密码,避免误操作
form.payPassword = '';
selectedUserText.value = '';
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
// 新增时确保表单是干净的默认值
resetFields();
form.payPassword = '';
form.type = 0;
form.status = 0;
form.comments = '';
form.sortNumber = 100;
selectedUserText.value = '';
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
images.value = [];
selectedUserText.value = '';
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,359 @@
<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 === 'shopName'">
<div class="text-purple-500">{{ record.shopName || '-' }}</div>
</template>
<template v-if="column.key === 'type'">
<a-tag v-if="record.type === 0">分销商</a-tag>
<a-tag v-if="record.type === 1" color="orange">门店</a-tag>
<a-tag v-if="record.type === 2" color="purple">配送员</a-tag>
</template>
<template v-if="column.key === 'qrcode'">
<QrcodeOutlined :style="{fontSize: '24px'}" @click="openQrCode(record)" />
</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>
<!-- 编辑弹窗 -->
<ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 二维码预览 -->
<a-modal
v-model:visible="showQrModal"
:title="qrModalTitle"
:footer="null"
:width="380"
centered
destroy-on-close
>
<div style="display: flex; justify-content: center">
<a-image v-if="qrModalUrl" :src="qrModalUrl" :width="280" :preview="false" />
</div>
<div style="display: flex; justify-content: center; margin-top: 12px">
<a-space>
<a-button @click="copyQrUrl">复制链接</a-button>
<a-button type="primary" @click="openQrInNewTab">打开原图</a-button>
</a-space>
</div>
</a-modal>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, QrcodeOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据
const current = ref<ShopDealerUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示二维码弹窗
const showQrModal = ref(false);
const qrModalUrl = ref<string>('');
const qrModalTitle = ref<string>('二维码');
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
const getQrCodeUrl = (userId?: number) => {
return `https://guilixu-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${userId ?? ''}`;
};
const openQrCode = (row: ShopDealerUser) => {
if (!row.userId) {
message.warning('缺少用户ID无法生成二维码');
return;
}
qrModalUrl.value = getQrCodeUrl(row.userId);
qrModalTitle.value = row.realName ? `${row.realName} 的二维码` : `UID_${row.userId} 二维码`;
showQrModal.value = true;
};
const copyQrUrl = async () => {
if (!qrModalUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(qrModalUrl.value);
message.success('已复制');
} catch (e) {
message.error('复制失败,请手动复制');
}
};
const openQrInNewTab = () => {
if (!qrModalUrl.value) {
return;
}
window.open(qrModalUrl.value, '_blank');
};
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 2;
return pageShopDealerUser({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 120
},
{
title: '所属门店',
dataIndex: 'shopName',
key: 'shopName'
},
{
title: '真实姓名',
dataIndex: 'realName',
key: 'realName'
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
// {
// title: '工资',
// dataIndex: 'money',
// key: 'money',
// width: 120
// },
// {
// title: '已冻结',
// dataIndex: 'freezeMoney',
// key: 'freezeMoney',
// width: 120
// },
{
title: '工资统计',
dataIndex: 'totalMoney',
key: 'totalMoney',
width: 120
},
// {
// title: '推荐人用户ID',
// dataIndex: 'refereeId',
// key: 'refereeId',
// width: 120
// },
// {
// title: '成员数量(一级)',
// dataIndex: 'firstNum',
// key: 'firstNum',
// width: 120
// },
// {
// title: '成员数量(二级)',
// dataIndex: 'secondNum',
// key: 'secondNum',
// width: 120
// },
// {
// title: '成员数量(三级)',
// dataIndex: 'thirdNum',
// key: 'thirdNum',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
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?: ShopDealerUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(row.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);
removeBatchShopDealerUser(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: ShopDealerUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerUser'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,51 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="姓名|电话"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
import useSearch from "@/utils/use-search";
import {ShopDealerUserParam} from "@/api/shop/shopDealerUser/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 表单数据
const { where } = useSearch<ShopDealerUserParam>({
keywords: '',
userId: undefined,
mobile: undefined,
realName: undefined
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,463 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:confirm-loading="loading"
: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-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
allow-clear
placeholder="店铺名称"
v-model:value="form.dealerName"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="小区名称" name="community">
<SelectCommunity
:key="`community`"
:value="form.community"
:placeholder="`选择小区`"
@done="onCommunity"
/>
</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="24" v-if="!isUpdate">
<a-form-item label="关联用户" name="userId">
<SelectUser
:key="selectedUserText"
:value="selectedUserText"
:placeholder="`选择用户`"
@done="onChooseUser"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="form.type" :disabled="true" placeholder="请选择类型">
<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-col>
<a-col :span="12">
<a-form-item label="手机号" name="mobile">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
:disabled="true"
v-model:value="form.mobile"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="真实姓名" name="realName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入真实姓名"
:disabled="true"
v-model:value="form.realName"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item-->
<!-- label="支付密码"-->
<!-- name="payPassword"-->
<!-- :help="isUpdate ? '留空表示不修改' : undefined"-->
<!-- >-->
<!-- <a-input-password-->
<!-- allow-clear-->
<!-- :maxlength="20"-->
<!-- placeholder="请输入支付密码"-->
<!-- v-model:value="form.payPassword"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<a-col :span="12">
<a-form-item label="头像" name="image">
<a-image :src="form.avatar" :width="50" :preview="false" />
</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="money">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.money"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="已冻结" name="freezeMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.freezeMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="累积提现" name="totalMoney">
<a-input-number
class="ele-fluid"
:min="0"
:precision="2"
stringMode
placeholder="0.00"
:disabled="true"
v-model:value="form.totalMoney"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="佣金比例" name="rate">
<a-input-number
class="ele-fluid"
:min="0"
:max="1"
:precision="4"
stringMode
:disabled="true"
placeholder="例如 0.007"
v-model:value="form.rate"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="12">-->
<!-- <a-form-item label="单价" name="price">-->
<!-- <a-input-number-->
<!-- class="ele-fluid"-->
<!-- :min="0"-->
<!-- :precision="2"-->
<!-- stringMode-->
<!-- placeholder="0.00"-->
<!-- v-model:value="form.price"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16">-->
<!-- <a-col :span="12">-->
<!-- <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-col>-->
<!-- <a-col :span="12">-->
<!-- <a-form-item label="备注" name="comments">-->
<!-- <a-textarea-->
<!-- :rows="3"-->
<!-- :maxlength="200"-->
<!-- show-count-->
<!-- placeholder="请输入备注"-->
<!-- v-model:value="form.comments"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-col>-->
<!-- </a-row>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, toDateString, uuid } from 'ele-admin-pro';
import { addShopDealerUser, updateShopDealerUser } from '@/api/shop/shopDealerUser';
import { ShopDealerUser } from '@/api/shop/shopDealerUser/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 type { User } from '@/api/system/user/model';
import SelectCommunity from '@/components/SelectCommunity/index.vue';
import {ShopCommunity} from "@/api/shop/shopCommunity/model";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopDealerUser | 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<ShopDealerUser>({
id: undefined,
userId: undefined,
avatar: undefined,
type: 0,
dealerName: undefined,
community: undefined,
realName: undefined,
mobile: undefined,
payPassword: '',
money: undefined,
freezeMoney: undefined,
totalMoney: undefined,
rate: undefined,
price: undefined,
refereeId: undefined,
firstNum: undefined,
secondNum: undefined,
thirdNum: undefined,
qrcode: undefined,
isDelete: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const selectedUserText = ref<string>('');
// 表单验证规则
const rules = reactive({
userId: [
{
validator: (_rule: unknown, value: number | undefined) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请选择关联用户'));
}
return Promise.resolve();
},
trigger: 'change'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择类型',
trigger: 'change'
}
],
dealerName: [
{
required: true,
type: 'string',
message: '请填写店铺名称',
trigger: 'blur'
}
],
community: [
{
required: true,
type: 'string',
message: '请选择所在小区',
trigger: 'blur'
}
],
// mobile: [
// {
// required: true,
// type: 'string',
// message: '请填写手机号',
// trigger: 'blur'
// },
// {
// pattern: /^1\\d{10}$/,
// message: '手机号格式不正确',
// trigger: 'blur'
// }
// ],
payPassword: [
{
validator: (_rule: unknown, value: string) => {
if (!isUpdate.value && !value) {
return Promise.reject(new Error('请输入支付密码'));
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
const onChooseUser = (user?: User) => {
if (!user) {
selectedUserText.value = '';
form.userId = undefined;
return;
}
form.userId = user.userId;
// 新增时尽量帮用户填充,避免重复输入
if (!form.realName) {
form.realName = user.realName ?? user.nickname;
}
if (!form.mobile) {
form.mobile = user.phone ?? user.mobile;
}
const name = user.realName ?? user.nickname ?? '';
const phone = user.phone ?? user.mobile ?? '';
selectedUserText.value = phone ? `${name}${phone}` : name;
};
const onCommunity = (data: ShopCommunity) => {
form.community = data.name;
}
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
// 不在弹窗里编辑的字段不提交避免误更新如自增ID、删除标识等
const {
isDelete,
tenantId,
createTime,
updateTime,
...rest
} = form;
const formData: ShopDealerUser = { ...rest };
// userId 新增需要,编辑不允许修改
if (isUpdate.value) {
delete formData.userId;
}
// 编辑时留空表示不修改密码
if (isUpdate.value && !formData.payPassword) {
delete formData.payPassword;
}
const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
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) {
formRef.value?.clearValidate();
images.value = [];
if (props.data) {
assignObject(form, props.data);
// 不回显密码,避免误操作
form.payPassword = '';
selectedUserText.value = '';
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
// 新增时确保表单是干净的默认值
resetFields();
form.payPassword = '';
form.type = 0;
form.status = 0;
form.comments = '';
form.sortNumber = 100;
selectedUserText.value = '';
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
images.value = [];
selectedUserText.value = '';
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,370 @@
<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 === 'dealerName'">
<div class="text-orange-500">{{ record.dealerName || '-' }}</div>
</template>
<template v-if="column.key === 'type'">
<a-tag v-if="record.type === 0">分销商</a-tag>
<a-tag v-if="record.type === 1" color="orange">门店</a-tag>
<a-tag v-if="record.type === 2" color="purple">配送员</a-tag>
</template>
<template v-if="column.key === 'qrcode'">
<QrcodeOutlined :style="{fontSize: '24px'}" @click="openQrCode(record)" />
</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>
<!-- 编辑弹窗 -->
<ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 二维码预览 -->
<a-modal
v-model:visible="showQrModal"
:title="qrModalTitle"
:footer="null"
:width="380"
centered
destroy-on-close
>
<div style="display: flex; justify-content: center">
<a-image v-if="qrModalUrl" :src="qrModalUrl" :width="280" :preview="false" />
</div>
<div style="display: flex; justify-content: center; margin-top: 12px">
<a-space>
<a-button @click="copyQrUrl">复制链接</a-button>
<a-button type="primary" @click="openQrInNewTab">打开原图</a-button>
</a-space>
</div>
</a-modal>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, QrcodeOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopDealerUser[]>([]);
// 当前编辑数据
const current = ref<ShopDealerUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示二维码弹窗
const showQrModal = ref(false);
const qrModalUrl = ref<string>('');
const qrModalTitle = ref<string>('二维码');
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
const getQrCodeUrl = (userId?: number) => {
return `https://guilixu-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${userId ?? ''}`;
};
const openQrCode = (row: ShopDealerUser) => {
if (!row.userId) {
message.warning('缺少用户ID无法生成二维码');
return;
}
qrModalUrl.value = getQrCodeUrl(row.userId);
qrModalTitle.value = row.realName ? `${row.realName} 的二维码` : `UID_${row.userId} 二维码`;
showQrModal.value = true;
};
const copyQrUrl = async () => {
if (!qrModalUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(qrModalUrl.value);
message.success('已复制');
} catch (e) {
message.error('复制失败,请手动复制');
}
};
const openQrInNewTab = () => {
if (!qrModalUrl.value) {
return;
}
window.open(qrModalUrl.value, '_blank');
};
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.type = 1;
return pageShopDealerUser({
...where,
...orders,
page,
limit
});
};
// 完整的列配置(包含所有字段)
const columns = ref<ColumnItem[]>([
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 90,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 120
},
{
title: '店铺名称',
dataIndex: 'dealerName',
key: 'dealerName'
},
{
title: '所属小区',
dataIndex: 'community',
key: 'community'
},
{
title: '真实姓名',
dataIndex: 'realName',
key: 'realName'
},
{
title: '手机号',
dataIndex: 'mobile',
key: 'mobile'
},
{
title: '可提现',
dataIndex: 'money',
key: 'money',
width: 120
},
{
title: '已冻结',
dataIndex: 'freezeMoney',
key: 'freezeMoney',
width: 120
},
{
title: '累积提现',
dataIndex: 'totalMoney',
key: 'totalMoney',
width: 120
},
// {
// title: '推荐人用户ID',
// dataIndex: 'refereeId',
// key: 'refereeId',
// width: 120
// },
// {
// title: '成员数量(一级)',
// dataIndex: 'firstNum',
// key: 'firstNum',
// width: 120
// },
// {
// title: '成员数量(二级)',
// dataIndex: 'secondNum',
// key: 'secondNum',
// width: 120
// },
// {
// title: '成员数量(三级)',
// dataIndex: 'thirdNum',
// key: 'thirdNum',
// width: 120
// },
{
title: '专属二维码',
dataIndex: 'qrcode',
key: 'qrcode',
align: 'center'
},
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// width: 120
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// width: 200,
// 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?: ShopDealerUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopDealerUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ShopDealerUser) => {
const hide = message.loading('请求中..', 0);
removeShopDealerUser(row.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);
removeBatchShopDealerUser(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: ShopDealerUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ShopDealerUser'
};
</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 { importShopDealerOrder } from '@/api/shop/shopDealerOrder';
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;
importShopDealerOrder(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,209 @@
<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 type="dashed" @click="handleExport">导出xls</a-button>
<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 { message } from 'ant-design-vue';
import { utils, writeFile } from 'xlsx';
import Import from './Import.vue';
import useSearch from '@/utils/use-search';
import { getTenantId } from '@/utils/domain';
import { pageShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw';
import type {
ShopDealerWithdraw,
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: '',
page: 1,
limit: 5000
});
const list = ref<ShopDealerWithdraw[]>([]);
// 搜索
const handleSearch = () => {
const searchParams = { ...where };
// 清除空值
Object.keys(searchParams).forEach((key) => {
if (searchParams[key] === '' || searchParams[key] === undefined) {
delete searchParams[key];
}
});
emit('search', searchParams);
};
const toMoney = (val: unknown) => {
const n = Number.parseFloat(String(val ?? '0'));
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
};
const toApplyStatus = (val: unknown) => {
const n = Number(val);
if (n === 10) return '待审核';
if (n === 20) return '审核通过';
if (n === 30) return '驳回/取消';
if (n === 40) return '已打款';
return '';
};
const toPayType = (val: unknown) => {
const n = Number(val);
if (n === 10) return '微信/商家转账';
if (n === 20) return '支付宝';
if (n === 30) return '银行卡';
return '';
};
// 导出
const handleExport = async () => {
const array: (string | number)[][] = [
[
'订单号',
'用户',
'手机号',
'提现金额',
'打款方式',
'申请状态',
'审核/打款时间',
'创建时间',
'备注',
'租户ID'
]
];
const params = { ...where };
// 清除空值,避免把空字符串传给后端
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
delete params[key];
}
});
params.page = 1;
params.limit = 5000;
await pageShopDealerWithdraw(params)
.then((data) => {
list.value = data?.list || [];
list.value.forEach((d: ShopDealerWithdraw) => {
array.push([
d.id ?? '',
`${d.realName || d.nickname || '-'}(${d.userId ?? '-'})`,
d.phone ?? '',
toMoney(d.money),
toPayType(d.payType),
toApplyStatus(d.applyStatus),
d.auditTime ?? '',
d.createTime ?? '',
d.comments ?? '',
d.tenantId ?? ''
]);
});
const sheetName = `bak_shop_dealer_withdraw_${getTenantId()}`;
const workbook = { SheetNames: [sheetName], Sheets: {} as any };
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
sheet['!cols'] = [
{ wch: 10 },
{ wch: 20 },
{ wch: 14 },
{ wch: 12 },
{ wch: 14 },
{ wch: 12 },
{ wch: 20 },
{ wch: 20 },
{ wch: 24 },
{ wch: 10 }
];
message.loading('正在导出...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
message.destroy();
}, 1000);
})
.catch((e: any) => {
message.destroy();
message.error(e?.message ?? String(e));
});
};
// 重置搜索
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 && form.payType !== 10"
>
<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,460 @@
<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>
<a-tag v-if="record.applyStatus === 50" color="error">已取消</a-tag>
</template>
<template v-if="column.key === 'userInfo'">
<a-space>
<a-avatar :src="record.avatar" />
<div class="flex flex-col">
<div>
<span>{{ record.realName || '未实名认证' }}</span>
<span class="text-gray-400">ID{{ record.userId }}</span>
</div>
<div><span class="text-gray-400">{{ record.phone }}</span></div>
</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>
</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: '订单号',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
fixed: 'left'
},
// {
// title: '用户ID',
// dataIndex: 'userId',
// key: 'userId',
// align: 'center',
// width: 90,
// fixed: 'left'
// },
{
title: '收款方式',
dataIndex: 'paymentInfo',
key: 'paymentInfo',
align: 'center',
width: 180
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo'
},
{
title: '转账金额',
dataIndex: 'money',
key: 'money',
align: 'center',
customRender: ({ text }) => {
const amount = parseFloat(text || '0').toFixed(2);
return {
type: 'span',
children: `¥${amount}`
};
}
},
// {
// 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,33 @@
<!-- 搜索表单 -->
<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';
const props = withDefaults(
defineProps<{
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: any): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
</script>

View File

@@ -0,0 +1,344 @@
<!-- 编辑弹窗 -->
<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="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="image">
<SelectFile
:placeholder="'请选择封面图'"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="banner图" name="banner">
<SelectFile
:placeholder="'请选择banner图'"
:limit="1"
:data="banners"
@done="chooseBanner"
@del="onDeleteBanner"
/>
</a-form-item>
<a-form-item label="赛事日期" name="eventDate">
<a-date-picker
show-time
format="YYYY-MM-DD HH:mm:ss"
class="ele-fluid"
placeholder="请选择赛事日期"
v-model:value="eventDateValue"
@change="onEventDateChange"
/>
</a-form-item>
<a-form-item label="赛事地点" name="location">
<a-input
allow-clear
placeholder="请输入赛事地点"
v-model:value="form.location"
/>
</a-form-item>
<a-form-item label="报名费(元)" name="entryFee">
<a-input-number
:min="0"
:precision="2"
class="ele-fluid"
placeholder="0表示免费"
v-model:value="form.entryFee"
/>
</a-form-item>
<a-form-item label="名额上限" name="maxParticipants">
<a-input-number
:min="0"
class="ele-fluid"
placeholder="0表示不限名额"
v-model:value="form.maxParticipants"
/>
</a-form-item>
<a-form-item label="尺码选项" name="sizeOptions">
<a-input
allow-clear
placeholder="如: XS,S,M,L,XL,XXL逗号分隔不需尺码可留空"
v-model:value="form.sizeOptions"
/>
</a-form-item>
<a-form-item label="报名必填项">
<a-checkbox-group v-model:value="requiredFields">
<a-checkbox :value="1">姓名</a-checkbox>
<a-checkbox :value="2">手机号</a-checkbox>
<a-checkbox :value="3">身份证</a-checkbox>
<a-checkbox :value="4">尺码</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="赛事描述" name="description">
<a-textarea
allow-clear
:rows="3"
placeholder="请输入赛事描述"
v-model:value="form.description"
/>
</a-form-item>
<a-form-item label="是否热门" name="isHot">
<a-radio-group v-model:value="form.isHot">
<a-radio :value="0"></a-radio>
<a-radio :value="1"></a-radio>
</a-radio-group>
</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 :value="3">已结束</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { useThemeStore } from '@/store/modules/theme';
import { addShopEvent, updateShopEvent } from '@/api/shop/shopEvent';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import dayjs from 'dayjs';
// 获取主题状态
const themeStore = useThemeStore();
const styleResponsive = ref(true);
const props = defineProps<{
visible: boolean;
data?: any | 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 banners = ref<ItemType[]>([]);
// 日期相关
const eventDateValue = ref<dayjs.Dayjs | null>(null);
// 必填项复选框
const requiredFields = ref<number[]>([]);
const form = reactive({
id: undefined as number | undefined,
name: undefined as string | undefined,
description: undefined as string | undefined,
image: undefined as string | undefined,
banner: undefined as string | undefined,
eventDate: undefined as string | undefined,
location: undefined as string | undefined,
maxParticipants: 0 as number,
entryFee: 0 as number,
sizeOptions: undefined as string | undefined,
requireName: 0 as number,
requirePhone: 0 as number,
requireIdCard: 0 as number,
requireSize: 0 as number,
status: 0 as number,
isHot: 0 as number,
tenantId: undefined as number | undefined,
createTime: undefined as string | undefined,
updateTime: undefined as string | undefined
});
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const rules = reactive({
name: [
{
required: true,
type: 'string',
message: '请填写赛事名称',
trigger: 'blur'
}
],
location: [
{
required: true,
type: 'string',
message: '请填写赛事地点',
trigger: 'blur'
}
]
});
// 日期选择处理
const onEventDateChange = (_: any, dateString: string) => {
form.eventDate = dateString;
};
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 chooseBanner = (data: FileRecord) => {
banners.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.banner = data.path;
};
const onDeleteBanner = (index: number) => {
banners.value.splice(index, 1);
form.banner = '';
};
// 监听必填项变化
watch(requiredFields, (val) => {
form.requireName = val.includes(1) ? 1 : 0;
form.requirePhone = val.includes(2) ? 1 : 0;
form.requireIdCard = val.includes(3) ? 1 : 0;
form.requireSize = val.includes(4) ? 1 : 0;
});
const { resetFields } = Form.useForm;
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
// 根据是否更新选择接口
const saveOrUpdate = isUpdate.value ? updateShopEvent : addShopEvent;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e: Error) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const isUpdate = computed(() => !!props.data?.id);
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
banners.value = [];
if (props.data) {
assignObject(form, props.data);
if (props.data.image) {
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
});
}
if (props.data.banner) {
banners.value.push({
uid: uuid(),
url: props.data.banner,
status: 'done'
});
}
// 解析日期
if (props.data.eventDate) {
eventDateValue.value = dayjs(props.data.eventDate);
}
// 解析必填项
requiredFields.value = [];
if (props.data.requireName === 1) requiredFields.value.push(1);
if (props.data.requirePhone === 1) requiredFields.value.push(2);
if (props.data.requireIdCard === 1) requiredFields.value.push(3);
if (props.data.requireSize === 1) requiredFields.value.push(4);
} else {
// 重置表单
form.id = undefined;
form.name = undefined;
form.description = undefined;
form.image = undefined;
form.banner = undefined;
form.eventDate = undefined;
form.location = undefined;
form.maxParticipants = 0;
form.entryFee = 0;
form.sizeOptions = undefined;
form.requireName = 0;
form.requirePhone = 0;
form.requireIdCard = 0;
form.requireSize = 0;
form.status = 0;
form.isHot = 0;
requiredFields.value = [];
}
} else {
// 重置表单
if (formRef.value) {
formRef.value.resetFields();
}
}
},
{ immediate: true }
);
</script>
<style scoped>
.form-fields-config {
border: 1px solid #d9d9d9;
border-radius: 8px;
padding: 16px;
background: #fafafa;
}
</style>

View File

@@ -0,0 +1,215 @@
<!-- 赛事报名记录列表 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="'【' + (event?.name || '') + '】报名记录'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
class="registration-table"
>
<template #toolbar>
<a-space>
<span class="统计信息">
{{ totalCount }} 人报名已缴费 {{ paidCount }} 待缴费 {{ unpaidCount }}
</span>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'payStatus'">
<a-tag v-if="record.payStatus === 0" color="orange">待缴费</a-tag>
<a-tag v-if="record.payStatus === 1" color="green">已缴费</a-tag>
<a-tag v-if="record.payStatus === 2" color="red">已取消</a-tag>
</template>
<template v-if="column.key === 'realName'">
<span>{{ record.realName || '-' }}</span>
</template>
<template v-if="column.key === 'phone'">
<span>{{ record.phone || '-' }}</span>
</template>
<template v-if="column.key === 'idCard'">
<span>{{ record.idCard ? record.idCard.substring(0, 4) + '****' + record.idCard.substring(record.idCard.length - 4) : '-' }}</span>
</template>
<template v-if="column.key === 'size'">
<span>{{ record.size || '-' }}</span>
</template>
<template v-if="column.key === 'entryFee'">
<span :class="record.entryFee > 0 ? 'text-price' : 'text-free'">
{{ record.entryFee > 0 ? '¥' + record.entryFee : '免费' }}
</span>
</template>
<template v-if="column.key === 'paidAt'">
<span>{{ record.paidAt ? record.paidAt.substring(0, 19) : '-' }}</span>
</template>
<template v-if="column.key === 'createTime'">
<span>{{ record.createTime ? record.createTime.substring(0, 19) : '-' }}</span>
</template>
</template>
</ele-pro-table>
</a-spin>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { pageShopEventRegistration } from '@/api/shop/shopEvent';
import type { ShopEvent, ShopEventRegistration, ShopEventRegistrationParam } from '@/api/shop/shopEvent/model';
const props = defineProps<{
visible: boolean;
event?: ShopEvent | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const loading = ref(false);
const maxable = ref(true);
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 统计数据
const totalCount = ref(0);
const paidCount = ref(0);
const unpaidCount = ref(0);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders
}) => {
return pageShopEventRegistration({
...where,
...orders,
eventId: props.event?.id,
page,
limit
}).then((res) => {
// 统计
if (res.list) {
totalCount.value = res.total || res.list.length;
paidCount.value = res.list.filter((item: ShopEventRegistration) => item.payStatus === 1).length;
unpaidCount.value = res.list.filter((item: ShopEventRegistration) => item.payStatus === 0).length;
}
return res;
});
};
// 列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '姓名',
dataIndex: 'realName',
key: 'realName',
width: 100,
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
width: 120,
},
{
title: '身份证号',
dataIndex: 'idCard',
key: 'idCard',
width: 150,
},
{
title: '尺码',
dataIndex: 'size',
key: 'size',
width: 80,
align: 'center'
},
{
title: '报名费',
dataIndex: 'entryFee',
key: 'entryFee',
width: 100,
align: 'center'
},
{
title: '支付状态',
dataIndex: 'payStatus',
key: 'payStatus',
width: 100,
align: 'center'
},
{
title: '订单号',
dataIndex: 'orderNo',
key: 'orderNo',
width: 180,
ellipsis: true
},
{
title: '报名时间',
dataIndex: 'createTime',
key: 'createTime',
width: 160
},
{
title: '缴费时间',
dataIndex: 'paidAt',
key: 'paidAt',
width: 160
}
]);
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible && props.event?.id) {
// 刷新数据
setTimeout(() => {
tableRef?.value?.reload();
}, 100);
}
},
{ immediate: true }
);
</script>
<style scoped>
.统计信息 {
color: #666;
font-size: 14px;
}
.text-price {
color: #f5222d;
font-weight: 500;
}
.text-free {
color: #52c41a;
}
</style>

View File

@@ -0,0 +1,276 @@
<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="shop-event-table"
>
<template #toolbar>
<search @search="reload" :selection="selection" @add="openEdit" />
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" :height="50" fit="cover" />
</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="orange">已截止</a-tag>
<a-tag v-if="record.status === 3" color="red">已结束</a-tag>
</template>
<template v-if="column.key === 'entryFee'">
<span :class="record.entryFee > 0 ? 'text-price' : 'text-free'">
{{ record.entryFee > 0 ? '¥' + record.entryFee : '免费' }}
</span>
</template>
<template v-if="column.key === 'isHot'">
<a-tag v-if="record.isHot === 1" color="red">热门</a-tag>
<span v-else class="text-gray">-</span>
</template>
<template v-if="column.key === 'participants'">
<span>{{ record.paidCount || 0 }}/{{ record.maxParticipants || '不限' }}</span>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openRegistration(record)">报名记录</a>
<a-divider type="vertical" />
<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>
<!-- 编辑弹窗 -->
<ShopEventEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 报名记录弹窗 -->
<ShopEventRegistrationList
v-model:visible="showRegistration"
:event="currentEvent"
/>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref, computed } from 'vue';
import { message } 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 ShopEventEdit from './components/shopEventEdit.vue';
import ShopEventRegistrationList from './components/shopEventRegistrationList.vue';
import { pageShopEvent, removeShopEvent } from '@/api/shop/shopEvent';
import type { ShopEvent, ShopEventParam } from '@/api/shop/shopEvent/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopEvent[]>([]);
// 当前编辑数据
const current = ref<ShopEvent | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示报名记录弹窗
const showRegistration = ref(false);
// 当前赛事
const currentEvent = ref<ShopEvent | null>(null);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
return pageShopEvent({
...where,
...orders,
page,
limit
});
};
// 完整的列配置
const allColumns = ref<ColumnItem[]>([
{
title: '赛事ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '封面图',
dataIndex: 'image',
key: 'image',
width: 80,
},
{
title: '赛事名称',
dataIndex: 'name',
key: 'name',
ellipsis: true
},
{
title: '赛事日期',
dataIndex: 'eventDate',
key: 'eventDate',
width: 160
},
{
title: '赛事地点',
dataIndex: 'location',
key: 'location',
width: 150,
ellipsis: true
},
{
title: '报名费',
dataIndex: 'entryFee',
key: 'entryFee',
width: 100
},
{
title: '名额',
dataIndex: 'participants',
key: 'participants',
width: 100,
align: 'center'
},
{
title: '热门',
dataIndex: 'isHot',
key: 'isHot',
width: 80,
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 160,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => text ? toDateString(text, 'yyyy-MM-dd HH:mm') : '-'
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列
const defaultVisibleColumns = [
'name',
'eventDate',
'location',
'entryFee',
'participants',
'status',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: ShopEventParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopEvent) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开报名记录弹窗 */
const openRegistration = (row: ShopEvent) => {
currentEvent.value = row;
showRegistration.value = true;
};
/* 删除单个 */
const remove = (row: ShopEvent) => {
const hide = message.loading('请求中..', 0);
removeShopEvent(row.id!)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e: Error) => {
hide();
message.error(e.message);
});
};
/* 自定义行属性 */
const customRow = (record: ShopEvent) => {
return {
onClick: () => {
// console.log(record);
},
onDblclick: () => {
openEdit(record);
}
};
};
</script>
<script lang="ts">
export default {
name: 'ShopEvent'
};
</script>
<style lang="less" scoped>
.text-price {
color: #f5222d;
font-weight: 500;
}
.text-free {
color: #52c41a;
}
.text-gray {
color: #999;
}
</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,208 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="700"
: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: 6, 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>
</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,179 @@
<!-- 搜索表单 -->
<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-button type="dashed" class="ele-btn-icon" @click="openImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="handleExport">
导出xls
</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, DeleteOutlined, UploadOutlined } 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 { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
merchantId?: number;
navigationList?: ShopGoodsCategory[];
}>(),
{
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;
(e: 'import'): void;
(e: 'export'): void;
}>();
// 新增
const add = () => {
emit('add');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
// 打开导入
const openImport = () => {
emit('import');
};
// 导出
const handleExport = () => {
emit('export');
};
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>

View File

@@ -0,0 +1,83 @@
<template>
<ele-modal
:width="520"
:footer="null"
title="商品批量导入"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<CloudUploadOutlined />
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a :href="templateUrl" download="商品导入模板.xlsx">下载导入模板</a>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importShopGoods } from '@/api/shop/shopGoods';
import { MODULES_API_URL } from '@/config/setting';
defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const loading = ref(false);
const templateUrl = computed(() => {
return MODULES_API_URL + '/shop/shop-goods/import/template';
});
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;
importShopGoods(file)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,432 @@
<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"
@import="openImport"
@export="handleExport"
/>
</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"
/>
<!-- 导入弹窗 -->
<ShopGoodsImport
v-model:visible="showImport"
@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 ShopGoodsImport from './components/shop-goods-import.vue';
import { listShopGoods, pageShopGoods, removeShopGoods, removeBatchShopGoods, updateShopGoods } from '@/api/shop/shopGoods';
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model';
import { getPageTitle } from '@/utils/common';
import { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model';
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory';
import { utils, writeFile } from 'xlsx';
import dayjs from 'dayjs';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopGoods[]>([]);
// 当前编辑数据
const current = ref<ShopGoods | null>(null);
// 分类数据
const navigationList = ref<ShopGoodsCategory[]>();
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = 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 openImport = () => {
showImport.value = true;
};
/* 导出商品列表 */
const handleExport = async () => {
if (loading.value) {
return;
}
loading.value = true;
message.loading('正在准备导出数据...', 0);
try {
const array: (string | number)[][] = [
[
'商品ID',
'商品名称',
'价格',
'销量',
'库存',
'状态',
'推荐',
'创建时间'
]
];
const list = await listShopGoods({});
if (!list || list.length === 0) {
message.warning('没有数据可以导出');
loading.value = false;
return;
}
list.forEach((goods: ShopGoods) => {
const statusMap = ['已上架', '待上架', '待审核', '审核不通过'];
array.push([
`${goods.goodsId || ''}`,
`${goods.name || ''}`,
`${goods.price || 0}`,
`${goods.sales || 0}`,
`${goods.stock || 0}`,
statusMap[goods.status || 0] || '',
goods.recommend === 1 ? '是' : '否',
`${goods.createTime || ''}`
]);
});
const sheetName = `商品列表${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 },
{ wch: 30 },
{ wch: 12 },
{ wch: 10 },
{ wch: 10 },
{ wch: 12 },
{ wch: 8 },
{ wch: 20 }
];
message.destroy();
message.loading('正在生成Excel文件...', 0);
setTimeout(() => {
writeFile(workbook, `${sheetName}.xlsx`);
loading.value = false;
message.destroy();
message.success(`成功导出 ${list.length} 条记录`);
}, 1000);
} catch (error: any) {
loading.value = false;
message.destroy();
message.error(error.message || '导出失败,请重试');
}
};
/* 删除单个 */
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) {
listShopGoodsCategory({}).then((res) => {
navigationList.value = toTreeData({
data: res?.map((d) => {
d.value = d.categoryId;
d.label = d.title;
return d;
}),
idField: 'categoryId',
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,72 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-select
v-model:value="where.type"
placeholder="选择类型"
allow-clear
style="width: 120px"
@change="search"
>
<a-select-option :value="0">商城分类</a-select-option>
<a-select-option :value="1">外卖分类</a-select-option>
</a-select>
<a-input-search
v-model:value="where.keywords"
placeholder="搜索分类名称"
allow-clear
style="width: 200px"
@search="search"
@pressEnter="search"
/>
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon><PlusOutlined /></template>
<span>添加分类</span>
</a-button>
<a-button
danger
class="ele-btn-icon"
:disabled="!selection?.length"
@click="remove"
>
<template #icon><DeleteOutlined /></template>
<span>批量删除</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import type { ShopGoodsCategoryParam } from '@/api/shop/shopGoodsCategory/model';
import useSearch from '@/utils/use-search';
const props = withDefaults(
defineProps<{
selection?: any[];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ShopGoodsCategoryParam): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
const { where } = useSearch<ShopGoodsCategoryParam>({
type: undefined,
keywords: ''
});
const search = () => {
emit('search', where);
};
const add = () => {
emit('add');
};
const remove = () => {
emit('remove');
};
</script>

View File

@@ -0,0 +1,266 @@
<!-- 商品分类编辑弹窗 -->
<template>
<ele-modal
:width="560"
:visible="visible"
:maskClosable="false"
:title="isUpdate ? '编辑分类' : '添加分类'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ flex: '90px' }"
:wrapper-col="{ flex: '1' }"
>
<a-form-item label="分类名称" name="title">
<a-input
allow-clear
placeholder="请输入分类名称"
v-model:value="form.title"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="上级分类" name="parentId">
<a-tree-select
allow-clear
:tree-data="parentTreeData"
tree-default-expand-all
style="width: 100%"
placeholder="顶级分类(不选则为顶级)"
:value="form.parentId || undefined"
@update:value="(val?: number) => (form.parentId = val)"
/>
</a-form-item>
<a-form-item label="分类类型" name="type">
<a-radio-group v-model:value="form.type">
<a-radio :value="0">商城分类</a-radio>
<a-radio :value="1">外卖分类</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="分类图片" name="image">
<a-upload
:before-upload="handleImageUpload"
:show-upload-list="false"
accept="image/*"
>
<a-button v-if="!form.image">
<template #icon><UploadOutlined /></template>
上传图片
</a-button>
<div v-else class="flex items-center gap-2">
<img :src="form.image" style="width: 60px; height: 60px; object-fit: cover; border-radius: 4px; border: 1px solid #d9d9d9;" />
<a-button type="text" danger size="small" @click.stop="form.image = undefined">删除</a-button>
</div>
</a-upload>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
style="width: 100%"
placeholder="数值越小越靠前"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="分类标识" name="categoryCode">
<a-input
allow-clear
placeholder="可选shoes、clothing"
v-model:value="form.categoryCode"
style="width: 100%"
/>
</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="comments">
<a-textarea
:rows="3"
:maxlength="200"
placeholder="可选备注说明"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue';
import { message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import { assignObject, toTreeData } from 'ele-admin-pro';
import { addShopGoodsCategory, updateShopGoodsCategory } from '@/api/shop/shopGoodsCategory';
import { uploadFile } from '@/api/system/file';
import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model';
import type { FormInstance } from 'ant-design-vue/es/form';
const isUpdate = ref(false);
const props = defineProps<{
visible: boolean;
data?: ShopGoodsCategory | null;
categoryList?: ShopGoodsCategory[];
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const loading = ref(false);
const formRef = ref<FormInstance | null>(null);
const form = reactive<ShopGoodsCategory>({
categoryId: undefined,
categoryCode: undefined,
title: undefined,
type: 0,
image: undefined,
parentId: undefined,
path: undefined,
component: undefined,
pageId: undefined,
userId: undefined,
count: undefined,
sortNumber: 100,
comments: undefined,
hide: 0,
recommend: 0,
showIndex: 0,
merchantId: undefined,
status: 0,
deleted: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined
});
// 父级分类树数据
const parentTreeData = computed(() => {
if (!props.categoryList) return [];
// 排除自己和自己的子孙节点,防止循环引用
const excludeIds = new Set<number>();
const collectIds = (id?: number) => {
if (!id) return;
excludeIds.add(id);
const children = props.categoryList?.filter((c) => c.parentId === id) || [];
children.forEach((c) => collectIds(c.categoryId));
};
if (props.data?.categoryId) {
collectIds(props.data.categoryId);
}
const filtered = props.categoryList?.filter((c) => !excludeIds.has(c.categoryId || 0)) || [];
return toTreeData({
data: filtered.map((c) => ({
...c,
value: c.categoryId,
label: c.title,
title: c.title,
key: c.categoryId
})),
idField: 'categoryId',
parentIdField: 'parentId'
});
});
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules: Record<string, any> = {
title: [
{
required: true,
type: 'string',
message: '请填写分类名称',
trigger: 'blur'
}
],
type: [
{
required: true,
type: 'number',
message: '请选择分类类型',
trigger: 'change'
}
]
};
const save = () => {
if (!formRef.value) return;
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateShopGoodsCategory : addShopGoodsCategory;
saveOrUpdate({ ...form })
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
// 图片上传
const handleImageUpload = async (file: File) => {
const isImage = file.type?.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件');
return false;
}
try {
const res = await uploadFile(file);
if (res?.path) {
form.image = res.path;
}
} catch (e: any) {
message.error(e.message || '上传失败');
}
return false; // 阻止默认上传
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
categoryId: undefined,
categoryCode: undefined,
title: undefined,
type: 0,
image: undefined,
parentId: undefined,
sortNumber: 100,
comments: undefined,
status: 0
});
isUpdate.value = false;
}
} else {
formRef.value?.resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,275 @@
<!-- 商品分类管理 -->
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="categoryId"
: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"
@add="openEdit"
@remove="removeBatch"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'title'">
<a-space :style="{ paddingLeft: ((depthMap.get(record.categoryId || 0) || 0) * 20) + 'px' }">
<img v-if="record.image" :src="record.image" style="width: 24px; height: 24px; object-fit: cover; border-radius: 4px;" />
<span class="font-bold">{{ record.title }}</span>
</a-space>
</template>
<template v-if="column.key === 'type'">
<a-tag :color="record.type === 0 ? 'blue' : 'orange'">
{{ record.type === 0 ? '商城分类' : '外卖分类' }}
</a-tag>
</template>
<template v-if="column.key === 'parentId'">
{{ getParentName(record.parentId) }}
</template>
<template v-if="column.key === 'status'">
<a-switch
:checked="record.status === 0"
checked-children="正常"
un-checked-children="禁用"
size="small"
@change="(val: boolean) => toggleStatus(record, val)"
/>
</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>
<!-- 编辑弹窗 -->
<ShopGoodsCategoryEdit
v-model:visible="showEdit"
:data="current"
:categoryList="flatCategoryList"
@done="reload"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-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 ShopGoodsCategoryEdit from './components/shopGoodsCategoryEdit.vue';
import {
listShopGoodsCategory,
removeShopGoodsCategory,
removeBatchShopGoodsCategory,
updateShopGoodsCategory
} from '@/api/shop/shopGoodsCategory';
import type { ShopGoodsCategory, ShopGoodsCategoryParam } from '@/api/shop/shopGoodsCategory/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ShopGoodsCategory[]>([]);
// 当前编辑数据
const current = ref<ShopGoodsCategory | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 分类列表(扁平,用于父级选择)
const flatCategoryList = ref<ShopGoodsCategory[]>([]);
// 分类层级深度映射
const depthMap = ref<Map<number, number>>(new Map());
// 计算每个分类的层级深度
const computeDepth = (list: ShopGoodsCategory[]) => {
const map = new Map<number, number>();
const getDepth = (id?: number): number => {
if (!id || id === 0) return 0;
if (map.has(id)) return map.get(id)!;
const parent = list.find((c) => c.categoryId === id);
const d = parent ? getDepth(parent.parentId) + 1 : 0;
map.set(id, d);
return d;
};
list.forEach((c) => {
if (c.categoryId) {
map.set(c.categoryId, getDepth(c.categoryId));
}
});
depthMap.value = map;
};
// 表格数据源(不分页,全量展示便于查看层级)
const datasource: DatasourceFunction = ({ where, orders }) => {
return listShopGoodsCategory({ ...where, ...orders }).then((res) => {
const list = res || [];
flatCategoryList.value = list;
computeDepth(list);
return { list, count: list.length };
});
};
// 获取父级分类名称
const getParentName = (parentId?: number) => {
if (!parentId || parentId === 0) return '顶级分类';
const parent = flatCategoryList.value.find((c) => c.categoryId === parentId);
return parent?.title || '顶级分类';
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'categoryId',
key: 'categoryId',
align: 'center',
width: 70
},
{
title: '分类名称',
dataIndex: 'title',
key: 'title',
align: 'left',
minWidth: 160
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 100
},
{
title: '上级分类',
dataIndex: 'parentId',
key: 'parentId',
align: 'center',
width: 120
},
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 80,
sorter: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 90
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 120,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 140,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ShopGoodsCategoryParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ShopGoodsCategory) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 切换状态 */
const toggleStatus = (row: ShopGoodsCategory, val: boolean) => {
const newStatus = val ? 0 : 1;
updateShopGoodsCategory({ ...row, status: newStatus })
.then((msg) => {
message.success(msg);
row.status = newStatus;
})
.catch((e) => {
message.error(e.message);
});
};
/* 删除单个 */
const remove = (row: ShopGoodsCategory) => {
const hide = message.loading('请求中..', 0);
removeShopGoodsCategory(row.categoryId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) return;
const ids = selection.value.map((item) => item.categoryId);
const hide = message.loading('请求中..', 0);
removeBatchShopGoodsCategory(ids)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 行属性:双击编辑 */
const customRow = (record: ShopGoodsCategory) => {
return {
onDblclick: () => {
openEdit(record);
}
};
};
// 初始化由 ele-pro-table 自动触发 datasource 加载
</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,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,187 @@
<template>
<div class="app-container">
<el-card>
<div slot="header"><span>权益包兑换记录</span></div>
<!-- 搜索 -->
<el-form :inline="true" :model="queryParams" class="mb-20">
<el-form-item label="用户ID">
<el-input v-model="queryParams.userId" clearable style="width:150px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
<el-option label="待处理" :value="0" />
<el-option label="已发货" :value="1" />
<el-option label="已完成" :value="2" />
<el-option label="已取消" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="userId" label="用户ID" width="90" />
<el-table-column label="用户信息" width="130">
<template slot-scope="scope">
<div>{{ scope.row.userRealname }}</div>
<div style="color:#999;font-size:12px">{{ scope.row.userPhone }}</div>
</template>
</el-table-column>
<el-table-column prop="packageName" label="权益包" min-width="150" />
<el-table-column label="消耗" width="130">
<template slot-scope="scope">
<span v-if="scope.row.pointsCost > 0">{{ scope.row.pointsCost }}积分</span>
<span v-if="scope.row.balanceCost > 0" style="margin-left:4px">¥{{ scope.row.balanceCost }}</span>
</template>
</el-table-column>
<el-table-column label="收货地址" min-width="200">
<template slot-scope="scope">
<template v-if="scope.row.addressSnapshot">
{{ formatAddress(scope.row.addressSnapshot) }}
</template>
<span v-else style="color:#ccc">无需配送</span>
</template>
</el-table-column>
<el-table-column label="物流" width="200">
<template slot-scope="scope">
<template v-if="scope.row.expressNo">
<div>{{ scope.row.expressCompany }}</div>
<div style="color:#1890ff">{{ scope.row.expressNo }}</div>
</template>
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.status)" size="mini">
{{ statusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="兑换时间" width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.status === 0"
type="text"
@click="handleShip(scope.row)"
>发货</el-button>
<el-button
v-if="scope.row.status === 1"
type="text"
@click="handleComplete(scope.row)"
>标记完成</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.page"
:limit.sync="queryParams.limit"
@pagination="getList"
/>
</el-card>
<!-- 发货对话框 -->
<el-dialog :visible.sync="shipDialogVisible" title="填写发货信息" width="440px">
<el-form :model="shipForm" label-width="90px">
<el-form-item label="快递公司">
<el-input v-model="shipForm.expressCompany" placeholder="如:顺丰速运" />
</el-form-item>
<el-form-item label="快递单号">
<el-input v-model="shipForm.expressNo" placeholder="请输入快递单号" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="shipDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitShip">确认发货</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getBenefitExchangePage,
shipBenefitExchange,
completeBenefitExchange,
} from '@/api/shop/shopMemberBenefit';
export default {
name: 'ShopMemberBenefitExchange',
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: { page: 1, limit: 20, userId: '', status: '' },
shipDialogVisible: false,
shipForm: { id: null, expressCompany: '', expressNo: '' },
};
},
created() { this.getList(); },
methods: {
async getList() {
this.loading = true;
try {
const res = await getBenefitExchangePage(this.queryParams);
this.list = res.data.list || [];
this.total = res.data.total || 0;
} finally { this.loading = false; }
},
handleQuery() { this.queryParams.page = 1; this.getList(); },
resetQuery() { this.queryParams = { page: 1, limit: 20, userId: '', status: '' }; this.getList(); },
handleShip(row) {
this.shipForm = { id: row.id, expressCompany: '', expressNo: '' };
this.shipDialogVisible = true;
},
async submitShip() {
if (!this.shipForm.expressNo) {
this.$message.warning('请输入快递单号');
return;
}
await shipBenefitExchange(this.shipForm.id, {
expressCompany: this.shipForm.expressCompany,
expressNo: this.shipForm.expressNo,
});
this.$message.success('发货成功');
this.shipDialogVisible = false;
this.getList();
},
async handleComplete(row) {
await this.$confirm('确认标记为已完成?', '提示', { type: 'warning' });
await completeBenefitExchange(row.id);
this.$message.success('已完成');
this.getList();
},
formatAddress(snapshot) {
try {
const addr = JSON.parse(snapshot);
return `${addr.name} ${addr.phone} ${addr.province || ''}${addr.city || ''}${addr.district || ''}${addr.detail || ''}`;
} catch {
return snapshot;
}
},
statusText(s) {
return ['待处理', '已发货', '已完成', '已取消'][s] || '-';
},
statusType(s) {
return ['warning', 'primary', 'success', 'info'][s] || 'info';
},
},
};
</script>
<style scoped>
.mb-20 { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,244 @@
<template>
<div class="app-container">
<el-card>
<div slot="header" class="clearfix">
<span>会员权益包管理</span>
<el-button style="float: right" type="primary" @click="handleAdd">
+ 新增权益包
</el-button>
</div>
<!-- 搜索 -->
<el-form :inline="true" :model="queryParams" class="mb-20">
<el-form-item label="名称">
<el-input v-model="queryParams.name" placeholder="权益包名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
<el-option label="上架" :value="1" />
<el-option label="下架" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column prop="pointsCost" label="积分" width="80" />
<el-table-column prop="balanceCost" label="余额(元)" width="90">
<template slot-scope="scope">¥{{ scope.row.balanceCost }}</template>
</el-table-column>
<el-table-column prop="stock" label="库存" width="80">
<template slot-scope="scope">
{{ scope.row.stock === -1 ? '不限' : scope.row.stock }}
</template>
</el-table-column>
<el-table-column prop="needShip" label="需配送" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.needShip ? 'success' : 'info'" size="mini">
{{ scope.row.needShip ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template slot-scope="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="70" />
<el-table-column label="操作" width="160" fixed="right">
<template slot-scope="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" style="color:#F56C6C" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.page"
:limit.sync="queryParams.limit"
@pagination="getList"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog :visible.sync="dialogVisible" :title="dialogTitle" width="640px">
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-form-item label="权益包名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="权益包描述" />
</el-form-item>
<el-form-item label="封面图片">
<el-input v-model="form.coverImage" placeholder="图片URL" />
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="所需积分" prop="pointsCost">
<el-input-number v-model="form.pointsCost" :min="0" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所需余额(元)">
<el-input-number v-model="form.balanceCost" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="库存(-1不限)">
<el-input-number v-model="form.stock" :min="-1" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="每人限兑(-1不限)">
<el-input-number v-model="form.exchangeLimit" :min="-1" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="需要配送">
<el-switch v-model="form.needShip" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效天数">
<el-input-number v-model="form.validDays" :min="0" placeholder="空=永久" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="标签">
<el-input v-model="form.tag" placeholder="如:热门/新品" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序权重">
<el-input-number v-model="form.sort" :min="0" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="1">上架</el-radio>
<el-radio :label="0">下架</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getBenefitPackagePage,
addBenefitPackage,
updateBenefitPackage,
deleteBenefitPackage,
} from '@/api/shop/shopMemberBenefit';
export default {
name: 'ShopMemberBenefitPackage',
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: { page: 1, limit: 20, name: '', status: '' },
dialogVisible: false,
dialogTitle: '新增权益包',
form: this.defaultForm(),
rules: {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
pointsCost: [{ required: true, message: '请输入积分', trigger: 'blur' }],
},
};
},
created() { this.getList(); },
methods: {
defaultForm() {
return {
id: null,
name: '',
description: '',
coverImage: '',
pointsCost: 0,
balanceCost: 0,
stock: -1,
exchangeLimit: 1,
needShip: 1,
validDays: null,
tag: '',
sort: 0,
status: 1,
};
},
async getList() {
this.loading = true;
try {
const res = await getBenefitPackagePage(this.queryParams);
this.list = res.data.list || [];
this.total = res.data.total || 0;
} finally { this.loading = false; }
},
handleQuery() { this.queryParams.page = 1; this.getList(); },
resetQuery() { this.queryParams = { page: 1, limit: 20, name: '', status: '' }; this.getList(); },
handleAdd() {
this.form = this.defaultForm();
this.dialogTitle = '新增权益包';
this.dialogVisible = true;
},
handleEdit(row) {
this.form = { ...row };
this.dialogTitle = '编辑权益包';
this.dialogVisible = true;
},
async handleStatusChange(row) {
await updateBenefitPackage({ id: row.id, status: row.status });
},
async handleDelete(row) {
await this.$confirm(`确认删除「${row.name}」?`, '提示', { type: 'warning' });
await deleteBenefitPackage(row.id);
this.$message.success('删除成功');
this.getList();
},
async handleSubmit() {
this.$refs.form.validate(async (valid) => {
if (!valid) return;
if (this.form.id) {
await updateBenefitPackage(this.form);
this.$message.success('修改成功');
} else {
await addBenefitPackage(this.form);
this.$message.success('添加成功');
}
this.dialogVisible = false;
this.getList();
});
},
},
};
</script>
<style scoped>
.mb-20 { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div class="app-container">
<el-card>
<div slot="header" class="clearfix">
<span>会员注册记录管理</span>
</div>
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="手机号">
<el-input
v-model="queryParams.phone"
placeholder="请输入手机号"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="处理中" :value="0" />
<el-option label="已完成" :value="1" />
<el-option label="已取消" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="list" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="dealerId" label="推荐人ID" width="100" />
<el-table-column prop="phone" label="下级手机号" width="150" />
<el-table-column prop="realName" label="下级姓名" width="120" />
<el-table-column label="会员费" width="100">
<template slot-scope="scope">
¥{{ scope.row.memberFee }}
</template>
</el-table-column>
<el-table-column label="注册费" width="100">
<template slot-scope="scope">
¥{{ scope.row.registerFee }}
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button
v-if="scope.row.memberFeeStatus === 0"
type="text"
@click="handleConfirmMemberFee(scope.row)"
>
确认收款
</el-button>
<el-button
v-if="scope.row.registerFeeStatus === 0"
type="text"
@click="handleConfirmRegisterFee(scope.row)"
>
确认支付
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.page"
:limit.sync="queryParams.limit"
@pagination="getList"
/>
</el-card>
</div>
</template>
<script>
import { getMemberRegisterList, confirmMemberFee, confirmRegisterFee } from '@/api/shop/shopMemberRegister';
export default {
name: 'ShopMemberRegister',
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: {
page: 1,
limit: 20,
phone: '',
status: ''
}
};
},
created() {
this.getList();
},
methods: {
async getList() {
this.loading = true;
try {
const res = await getMemberRegisterList(this.queryParams);
this.list = res.data.list || [];
this.total = res.data.total || 0;
} catch (error) {
console.error('查询失败', error);
} finally {
this.loading = false;
}
},
handleQuery() {
this.queryParams.page = 1;
this.getList();
},
resetQuery() {
this.queryParams = {
page: 1,
limit: 20,
phone: '',
status: ''
};
this.getList();
},
async handleConfirmMemberFee(row) {
this.$confirm('确认已收到会员费?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await confirmMemberFee(row.id);
this.$message.success('确认成功');
this.getList();
} catch (error) {
console.error('确认失败', error);
}
}).catch(() => {});
},
async handleConfirmRegisterFee(row) {
try {
await confirmRegisterFee(row.id);
this.$message.success('确认成功');
this.getList();
} catch (error) {
console.error('确认失败', error);
}
},
getStatusType(status) {
const map = { 0: 'warning', 1: 'success', 2: 'info' };
return map[status] || 'info';
},
getStatusText(status) {
const map = { 0: '处理中', 1: '已完成', 2: '已取消' };
return map[status] || '未知';
}
}
};
</script>
<style scoped>
.search-form {
margin-bottom: 20px;
}
</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>

Some files were not shown because too many files have changed in this diff Show More